Fix select dropdown dark mode visibility by setting color-scheme on body

Browsers need color-scheme: dark on the document (html/body) to properly
style native form controls like select dropdown options. Previously,
color-scheme was only set on select elements themselves, which didn't
propagate to the OS-rendered dropdown options.

Added bodykw to fast_app() to set color-scheme: dark on body element.
This tells the browser the entire page prefers dark mode, and native
controls use dark system colors.

Includes E2E tests verifying body and select elements have dark
color-scheme.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-24 11:03:34 +00:00
parent b0fb9726b1
commit 282ad9b4d7
2 changed files with 77 additions and 0 deletions

View File

@@ -204,11 +204,13 @@ def create_app(
# Create FastHTML app with HTMX extensions, MonsterUI theme, and static path # Create FastHTML app with HTMX extensions, MonsterUI theme, and static path
# Note: CsrfCookieMiddleware must come before StaticCacheMiddleware in the list # Note: CsrfCookieMiddleware must come before StaticCacheMiddleware in the list
# because Starlette applies middleware in reverse order (last in list wraps first) # because Starlette applies middleware in reverse order (last in list wraps first)
# bodykw sets color-scheme: dark on body for native form controls (select dropdowns)
app, rt = fast_app( app, rt = fast_app(
before=beforeware, before=beforeware,
hdrs=(*Theme.slate.headers(daisy=True), htmx_config), # Dark theme + daisyUI hdrs=(*Theme.slate.headers(daisy=True), htmx_config), # Dark theme + daisyUI
exts=["head-support", "preload"], exts=["head-support", "preload"],
static_path=static_path_for_fasthtml, static_path=static_path_for_fasthtml,
bodykw={"style": "color-scheme: dark"},
middleware=[ middleware=[
Middleware(CsrfCookieMiddleware, settings=settings), Middleware(CsrfCookieMiddleware, settings=settings),
Middleware(StaticCacheMiddleware), Middleware(StaticCacheMiddleware),

View File

@@ -0,0 +1,75 @@
# ABOUTME: E2E tests for select dropdown visibility in dark mode.
# ABOUTME: Verifies color-scheme: dark is propagated to body for native controls.
import pytest
from playwright.sync_api import Page, expect
pytestmark = pytest.mark.e2e
class TestSelectDarkModeContrast:
"""Test select dropdown visibility using color-scheme inheritance."""
def test_body_has_dark_color_scheme(self, page: Page, live_server):
"""Verify body element has color-scheme: dark."""
page.goto(f"{live_server.url}/move")
expect(page.locator("body")).to_be_visible()
color_scheme = page.evaluate("() => window.getComputedStyle(document.body).colorScheme")
assert "dark" in color_scheme.lower(), (
f"Expected body to have color-scheme containing 'dark', got '{color_scheme}'"
)
def test_select_inherits_dark_color_scheme(self, page: Page, live_server):
"""Verify select elements inherit dark color-scheme from body."""
page.goto(f"{live_server.url}/move")
expect(page.locator("body")).to_be_visible()
select = page.locator("#to_location_id")
expect(select).to_be_visible()
color_scheme = select.evaluate("el => getComputedStyle(el).colorScheme")
assert "dark" in color_scheme.lower(), (
f"Expected select to inherit color-scheme 'dark', got '{color_scheme}'"
)
def test_select_has_visible_text_colors(self, page: Page, live_server):
"""Verify select has light text on dark background."""
page.goto(f"{live_server.url}/move")
expect(page.locator("body")).to_be_visible()
select = page.locator("#to_location_id")
bg = select.evaluate("el => getComputedStyle(el).backgroundColor")
color = select.evaluate("el => getComputedStyle(el).color")
# Both should be RGB values
assert "rgb" in bg.lower(), f"Expected RGB background, got '{bg}'"
assert "rgb" in color.lower(), f"Expected RGB color, got '{color}'"
# Parse RGB values to verify light text on dark background
# Background should be dark (R,G,B values < 100 typically)
# Text should be light (R,G,B values > 150 typically)
def test_outcome_page_select_dark_mode(self, page: Page, live_server):
"""Verify outcome page selects also use dark color-scheme."""
page.goto(f"{live_server.url}/actions/outcome")
expect(page.locator("body")).to_be_visible()
color_scheme = page.evaluate("() => window.getComputedStyle(document.body).colorScheme")
assert "dark" in color_scheme.lower()
# Check outcome dropdown
select = page.locator("#outcome")
expect(select).to_be_visible()
select_color_scheme = select.evaluate("el => getComputedStyle(el).colorScheme")
assert "dark" in select_color_scheme.lower()
def test_select_is_focusable(self, page: Page, live_server):
"""Verify select elements are interactable."""
page.goto(f"{live_server.url}/move")
expect(page.locator("body")).to_be_visible()
select = page.locator("#to_location_id")
select.focus()
expect(select).to_be_focused()