From bf09bc5dcd8bed1d466633956b96ecb776fc76fa Mon Sep 17 00:00:00 2001 From: evnchn Date: Wed, 15 Apr 2026 16:16:32 +0800 Subject: [PATCH 1/4] Re-implement accessibility core: at_rule color support, prefers-reduced-motion - nicegui/elements/colors.{js,py}: at_rule parameter to scope colors by media query - nicegui/static/nicegui.css: prefers-reduced-motion kill-switch for transitions/animations/smooth-scroll - tests/test_colors.py: test_at_rule - main.py: ui.colors(primary='#317ABE', at_rule='@media (prefers-contrast: more)') Previously 30859c64 inadvertently reverted recent upstream CSS and main.py changes by wholesale-copying from a stale branch; this rewrites that commit cleanly. --- main.py | 1 + nicegui/elements/colors.js | 51 ++++++++++++++++++++++++++++++-------- nicegui/elements/colors.py | 3 +++ nicegui/static/nicegui.css | 8 ++++++ tests/test_colors.py | 10 ++++++++ 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index 2679d6b8e1..1e7fc3f46d 100755 --- a/main.py +++ b/main.py @@ -61,6 +61,7 @@ def _render_page(self, match: RouteMatch) -> bool: @ui.page('/documentation/{path:path}') @ui.page('/imprint_privacy') def _main_page() -> None: + ui.colors(primary='#317ABE', at_rule='@media (prefers-contrast: more)') ui.context.client.content.classes('p-0 gap-0') header.add_head_html() diff --git a/nicegui/elements/colors.js b/nicegui/elements/colors.js index 235974ed17..978762f628 100644 --- a/nicegui/elements/colors.js +++ b/nicegui/elements/colors.js @@ -1,15 +1,45 @@ export default { mounted() { - document.body.style.setProperty("--q-primary", this.primary); - document.body.style.setProperty("--q-secondary", this.secondary); - document.body.style.setProperty("--q-accent", this.accent); - document.body.style.setProperty("--q-dark", this.dark); - document.body.style.setProperty("--q-dark-page", this.darkPage); - document.body.style.setProperty("--q-positive", this.positive); - document.body.style.setProperty("--q-negative", this.negative); - document.body.style.setProperty("--q-info", this.info); - document.body.style.setProperty("--q-warning", this.warning); - applyColors(this.customColors); + if (!this.atRule) { + document.body.style.setProperty("--q-primary", this.primary); + document.body.style.setProperty("--q-secondary", this.secondary); + document.body.style.setProperty("--q-accent", this.accent); + document.body.style.setProperty("--q-dark", this.dark); + document.body.style.setProperty("--q-dark-page", this.darkPage); + document.body.style.setProperty("--q-positive", this.positive); + document.body.style.setProperty("--q-negative", this.negative); + document.body.style.setProperty("--q-info", this.info); + document.body.style.setProperty("--q-warning", this.warning); + applyColors(this.customColors); + return; + } + const colors = { + "--q-primary": this.primary, + "--q-secondary": this.secondary, + "--q-accent": this.accent, + "--q-dark": this.dark, + "--q-dark-page": this.darkPage, + "--q-positive": this.positive, + "--q-negative": this.negative, + "--q-info": this.info, + "--q-warning": this.warning, + }; + let css = Object.entries(colors) + .map(([k, v]) => ` body { ${k}: ${v} !important; }`) + .join("\n"); + for (const [color, value] of Object.entries(this.customColors || {})) { + const name = color.replaceAll("_", "-"); + const varName = "--q-" + name; + css += `\n body { ${varName}: ${value} !important; }`; + css += `\n .text-${name} { color: var(${varName}) !important; }`; + css += `\n .bg-${name} { background-color: var(${varName}) !important; }`; + } + this.styleEl = document.createElement("style"); + this.styleEl.innerHTML = `${this.atRule} {\n${css}\n}`; + document.head.appendChild(this.styleEl); + }, + unmounted() { + this.styleEl?.remove(); }, props: { primary: String, @@ -21,6 +51,7 @@ export default { negative: String, info: String, warning: String, + atRule: String, customColors: Object, }, }; diff --git a/nicegui/elements/colors.py b/nicegui/elements/colors.py index ec07b3479a..405b05cc77 100644 --- a/nicegui/elements/colors.py +++ b/nicegui/elements/colors.py @@ -16,6 +16,7 @@ def __init__(self, *, negative: str = DEFAULT_PROP | '#c10015', info: str = DEFAULT_PROP | '#31ccec', warning: str = DEFAULT_PROP | '#f2c037', + at_rule: str = '', **custom_colors: str) -> None: """Color Theming @@ -32,6 +33,7 @@ def __init__(self, *, :param negative: Negative color (default: "#c10015") :param info: Info color (default: "#31ccec") :param warning: Warning color (default: "#f2c037") + :param at_rule: CSS at-rule to limit when the colors apply (e.g. ``"@media (prefers-color-scheme: dark)"``) :param custom_colors: Custom color definitions for branding (needs ``ui.colors`` to be called before custom color is ever used, *added in version 2.2.0*) """ super().__init__() @@ -44,6 +46,7 @@ def __init__(self, *, self._props['negative'] = negative self._props['info'] = info self._props['warning'] = warning + self._props['at-rule'] = at_rule self._props['custom-colors'] = custom_colors QUASAR_COLORS.update({name.replace('_', '-') for name in custom_colors}) diff --git a/nicegui/static/nicegui.css b/nicegui/static/nicegui.css index 0438c4bc75..73bdf8eeaf 100644 --- a/nicegui/static/nicegui.css +++ b/nicegui/static/nicegui.css @@ -342,3 +342,11 @@ h6.q-timeline__title { position: absolute; right: 1.5em; } +@media (prefers-reduced-motion: reduce) { + html, + body * { + transition: none !important; + animation: none !important; + scroll-behavior: auto !important; + } +} diff --git a/tests/test_colors.py b/tests/test_colors.py index b32ab056d9..918f8405e1 100644 --- a/tests/test_colors.py +++ b/tests/test_colors.py @@ -32,3 +32,13 @@ def replace(): screen.click('Replace') screen.wait(0.5) assert screen.find_by_tag('button').value_of_css_property('background-color') == 'rgba(255, 0, 0, 1)' + + +def test_at_rule(screen: Screen): + @ui.page('/') + def page(): + ui.colors(primary='#ff0000', at_rule='@media (min-width: 0px)') + ui.button('Test Button') + + screen.open('/') + assert screen.find_by_tag('button').value_of_css_property('background-color') == 'rgba(255, 0, 0, 1)' From d005e7a60307f18ceb2b31fd899f22eb94402968 Mon Sep 17 00:00:00 2001 From: evnchn Date: Wed, 15 Apr 2026 16:16:32 +0800 Subject: [PATCH 2/4] Accessibility wiring in new website/components/ structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - website/components/hero_section.py: forced-colors:invert on mascot SVG - website/documentation/windows.py: forced-colors outline/opacity on code & browser windows - website/header.py: Tailwind variant for prefers-reduced-transparency on .q-header.fade Social icons (Phosphor/currentColor), logo wordmark (markdown), and star badge targets dropped — redesign removed them. --- website/components/hero_section.py | 2 +- website/documentation/windows.py | 7 ++++--- website/header.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/website/components/hero_section.py b/website/components/hero_section.py index d64935c67a..d4c78904e1 100644 --- a/website/components/hero_section.py +++ b/website/components/hero_section.py @@ -16,7 +16,7 @@ def create() -> None: ) with ui.column(align_items='center').classes('reveal'): ui.html(svg.HAPPY_FACE_SVG, sanitize=False) \ - .classes(f'hero-mascot size-40 stroke-[{d.BLUE}] stroke-2 mb-8') + .classes(f'hero-mascot size-40 stroke-[{d.BLUE}] forced-colors:invert stroke-2 mb-8') ui.markdown('Meet the *NiceGUI*.') \ .classes(f'{d.TEXT_HERO} font-semibold tracking-tighter leading-none [&_em]:not-italic [&_em]:{d.TEXT_BLUE} {d.TEXT_PRIMARY} -mb-2') ui.markdown(''' diff --git a/website/documentation/windows.py b/website/documentation/windows.py index a5590cc335..e3a18844e3 100644 --- a/website/documentation/windows.py +++ b/website/documentation/windows.py @@ -16,14 +16,15 @@ def code_window(code: str = '', *, title: str = 'main.py', language: str = 'python') -> ui.column: """Create a window for code. If code is empty, returns the body column for use as context manager.""" - with ui.column().classes(f'rounded-xl gap-0 min-w-0 {d.BG_CODE} code-window') as window: + with ui.column().classes(f'rounded-xl gap-0 min-w-0 {d.BG_CODE} code-window forced-colors:outline') as window: with _header_row(): phosphor_icon(ICONS.get(language, 'ph-file')).classes('text-base') ui.label(title) if code: ui.space() with ui.button(on_click=lambda: ui.clipboard.write(code)) \ - .props('flat round size=xs').classes('opacity-30 hover:opacity-100 transition-opacity'): + .props('flat round size=xs') \ + .classes('opacity-30 hover:opacity-100 forced-colors:opacity-100 transition-opacity'): phosphor_icon('ph-copy').classes('text-base') if code: ui.markdown(f'````{language}\n{remove_indentation(code)}\n````') \ @@ -43,7 +44,7 @@ def python_window(code: str = '', *, title: str = 'main.py') -> ui.column: def browser_window(content: Callable, *, tab: str | Callable | None = None, lazy: bool = True) -> ui.column: """Create a browser window.""" - with ui.column().classes(f'rounded-xl gap-0 {d.BG_SURFACE} {d.RING} browser-window') as window: + with ui.column().classes(f'rounded-xl gap-0 {d.BG_SURFACE} {d.RING} browser-window forced-colors:outline') as window: with _header_row(): if callable(tab): tab() diff --git a/website/header.py b/website/header.py index 590da49943..04fe76951d 100644 --- a/website/header.py +++ b/website/header.py @@ -81,6 +81,7 @@ def add_header(menu: ui.left_drawer) -> ui.button: f' [&.fade]:!bg-[color-mix(in_srgb,{d._BG_SURFACE_LIGHT}_80%,transparent)]' f' dark:[&.fade]:!bg-[color-mix(in_srgb,{d._BG_SURFACE_DARK}_80%,transparent)]' f' [&.fade]:backdrop-blur-[12px]' + f' media-[(prefers-reduced-transparency:reduce)]:[&.fade]:backdrop-blur-none' f' [&.fade]:!shadow-[0_1px_0_{d._BORDER_LIGHT}]' f' [&.fade]:dark:!shadow-[0_1px_0_{d._BORDER_DARK}]' f' [.q-layout:has(.q-drawer--standard:not(.q-layout--prevent-focus))_&]:!shadow-[0_1px_0_{d._BORDER_LIGHT}]' From 4b27a5c6c06f58eb4f759bb9601dd2848c2dd0e5 Mon Sep 17 00:00:00 2001 From: evnchn Date: Wed, 15 Apr 2026 20:24:37 +0800 Subject: [PATCH 3/4] Address Copilot: broaden prefers-reduced-motion selector Previously the rule targeted 'html, body *' which excludes the body element itself and pseudo-elements. Switch to '*, *::before, *::after' so animations/transitions on body and generated content also honor prefers-reduced-motion. --- nicegui/static/nicegui.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nicegui/static/nicegui.css b/nicegui/static/nicegui.css index 73bdf8eeaf..e970ca2e39 100644 --- a/nicegui/static/nicegui.css +++ b/nicegui/static/nicegui.css @@ -343,8 +343,9 @@ h6.q-timeline__title { right: 1.5em; } @media (prefers-reduced-motion: reduce) { - html, - body * { + *, + *::before, + *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; From 5f2abd36800221b78dfcf424775e2d4ec235d136 Mon Sep 17 00:00:00 2001 From: evnchn Date: Wed, 15 Apr 2026 21:21:05 +0800 Subject: [PATCH 4/4] Clean up prior custom-color styles when at_rule is set (Copilot discussion) --- nicegui/elements/colors.js | 10 ++++++++++ tests/test_colors.py | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/nicegui/elements/colors.js b/nicegui/elements/colors.js index 978762f628..e8518f5389 100644 --- a/nicegui/elements/colors.js +++ b/nicegui/elements/colors.js @@ -34,7 +34,17 @@ export default { css += `\n .text-${name} { color: var(${varName}) !important; }`; css += `\n .bg-${name} { background-color: var(${varName}) !important; }`; } + // Clear any prior NiceGUI-added color styles so earlier plain `ui.colors(...)` calls + // don't bleed through globally when this scoped at-rule block takes over. + // Mirrors the `[data-nicegui-custom-colors]` cleanup pattern in `applyColors` (static/nicegui.js). + document.head + .querySelectorAll("[data-nicegui-custom-colors], [data-nicegui-scoped-colors]") + .forEach((el) => el.remove()); + for (const key of Object.keys(colors)) { + document.body.style.removeProperty(key); + } this.styleEl = document.createElement("style"); + this.styleEl.dataset.niceguiScopedColors = ""; this.styleEl.innerHTML = `${this.atRule} {\n${css}\n}`; document.head.appendChild(this.styleEl); }, diff --git a/tests/test_colors.py b/tests/test_colors.py index 918f8405e1..36b34115a7 100644 --- a/tests/test_colors.py +++ b/tests/test_colors.py @@ -42,3 +42,14 @@ def page(): screen.open('/') assert screen.find_by_tag('button').value_of_css_property('background-color') == 'rgba(255, 0, 0, 1)' + + +def test_at_rule_supersedes_plain_colors(screen: Screen): + @ui.page('/') + def page(): + ui.colors(primary='red') + ui.colors(primary='blue', at_rule='@media (min-width: 0px)') + ui.button('Test Button') + + screen.open('/') + assert screen.find_by_tag('button').value_of_css_property('background-color') == 'rgba(0, 0, 255, 1)'