Skip to content

feat(theme): OS-follow color tokens via prefers-color-scheme (CL-8479) - #42

Merged
TheGreatAxios merged 8 commits into
mainfrom
cl-8479-theme-dark-default
Sep 21, 2026
Merged

TheGreatAxios merged 8 commits into
mainfrom
cl-8479-theme-dark-default

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

What

A fresh install with no JS (no ThemeProvider, no .dark class ever added)
stayed light even on a dark-OS host. Adds a
@media (prefers-color-scheme: dark) block in src/theme.css applying the
same dark tokens to :root whenever neither .dark nor an explicit .light
is present. ThemeProvider (applyThemeToRoot in src/lib/theme.ts) now
also toggles a .light class on explicit light mode, so an explicit light
choice on a dark-OS host is never clawed back by the media query — the
provider still wins in both directions.

dist/theme.css is generated from src/theme.css by bun run build
(cp src/theme.css dist/theme.css), so the two stay identical automatically.

README: documented the two import paths (styles.css vs theme.css), that
skipping both leaves components unstyled (not a fallback look), and the new
zero-JS dark-by-default behavior.

Verified

  • bun run typecheck — clean
  • bun run lint — clean
  • bun run dep-guard — clean
  • bun test — 337 pass, 0 fail (pre-existing act() warnings, unrelated)
  • bun run build — contrast gate passes all pairs in both light and dark

Not verified

  • No manual browser check of the media-query behavior on an actual dark-OS
    host (no zero-JS test harness in this repo for that).

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review

Verdict: approve with nits. Dark tokens via prefers-color-scheme plus applyThemeToRoot .light toggle. No CI. No prior reviews.

Should-fix: dark token block is duplicated byte-for-byte and will drift. Media query sets color tokens but not color-scheme: dark, so zero-JS dark can leave native controls light.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Verdict: approve the theme should-fixes; Toaster suite still red.

SHA 65f3636: color-scheme dark on media-query :root and .dark. Token blocks stay duplicated (light-dark() would break contrast-test); lockstep test fails if they diverge.

Full bun test: 335 pass, 3 fail in tests/ui/toast.test.tsx (leftover-toast Expected 1 Received 2). Pre-existing vs this theme diff; a follow-up is in flight on this branch so CI is actually green.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Verdict: approve. SHA 99f2eef.

Toaster isolation: sonnerToast.dismiss() in afterEach before unmount. bun test 338 pass, 0 fail. Theme color-scheme and token lockstep unchanged. typecheck/lint 0.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review (Draper + Emil)

Verdict: approve with docs nits.

Zero-JS OS dark is the right default. color-scheme: dark and the token lockstep test are correct.

  • Title/README overclaim “dark by default.” This is OS-follow when neither .dark nor .light is set. Removing .dark is no longer “light”; hosts must add .light or use ThemeProvider.
  • OS path is tokens only. @custom-variant dark still needs a .dark ancestor, so consumer dark: utilities stay light until JS.
  • Explicit-light on a dark OS first-paints dark, then .light. Document a blocking snippet; don’t animate the swap.

Docs-only follow-up on this branch.

@TheGreatAxios TheGreatAxios changed the title feat(theme): Corbits dark mode by default via prefers-color-scheme (CL-8479) feat(theme): OS-follow color tokens via prefers-color-scheme (CL-8479) Sep 21, 2026
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Re-review

Nits landed at 7a438c4. README/title: OS-follow; .light required for explicit light; dark: still needs .dark. bun test 338 pass.

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach is sound. Duplicating the .dark token block inside the media query — with a drift test asserting the two stay token-identical — is the right call given light-dark() would break the contrast gate's hex parsing. The .light escape hatch closes the real hole (explicit light on a dark-OS host), and ThemeProvider's live matchMedia listener means system mode still follows the OS after mount.

Ride-alongs (README install rewrite, sonner store dismissal in toast tests) are unrelated but harmless. Merge.

Native CSS cannot share one declaration list between .dark (any OS) and a
media-qualified :root without light-dark(), which would break the contrast
gate. Keep the two blocks token-identical, add color-scheme: dark to both,
and fail the suite if they drift.
Sonner replays active toasts onto the next Toaster subscriber, so leftover
confirmations from a prior case stacked (Expected 1, Received 2).
Removing .dark is not light; hosts need .light. The media-query path
sets tokens and color-scheme only — dark: utilities still need a .dark
ancestor.
@TheGreatAxios
TheGreatAxios force-pushed the cl-8479-theme-dark-default branch from 7a438c4 to 9619dad Compare September 21, 2026 18:25
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review pass — rebased onto post-prune main + fixes applied

Rebased after the prune (#55) and test co-location (#50): generated exports regenerated, theme.test.ts css path fixed for its new src/lib/ home.

Persona review findings — all resolved

  • Ladle theme toggle regression (greybeard, emil): the global light/dark/auto switch only toggled .dark; with the stylesheet's new OS-following media path, "light" still rendered dark on dark-OS dev boxes. Fixed — .ladle/components.tsx now pins .light explicitly.
  • Stale contract docs (draper, greybeard): ARCHITECTURE, IMPLEMENTATION, PRODUCT, README, and CHANGELOG still described "dark is opt-in .dark, removing it means light." All rewritten to document the real contract: classless root follows prefers-color-scheme, .light on documentElement is the explicit opt-out, dark: utilities still need a real .dark ancestor.
  • Anti-flash snippet inverted the bug (emil): the README snippet added .light unconditionally, pinning light for dark-OS users too. Now conditional on the stored choice.

Validation

227 tests, typecheck, lint, dep-guard, full build (js/types/css), contrast gate — all green at 17c13a2.

@TheGreatAxios
TheGreatAxios merged commit 1a784cf into main Sep 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant