Skip to content

fix(frontend): declare color-scheme, so dark mode gets dark browser furniture - #77

Merged
khafifithebork merged 1 commit into
masterfrom
fix/declare-color-scheme
Sep 27, 2026
Merged

khafifithebork merged 1 commit into
masterfrom
fix/declare-color-scheme

Conversation

@khafifithebork

Copy link
Copy Markdown
Owner

Split out of ADR-032 at the owner's instruction. That ADR asks a product question — should there be a manual theme toggle — and found a defect while asking. The defect needed no product decision, so it should not wait behind one.

The defect

globals.css swaps 17 tokens and two shadows inside a prefers-color-scheme query, and that is everything our stylesheet paints. color-scheme was never declared, so everything the browser paints itself stayed light: scrollbars, autofill, native video controls, select popups, date pickers.

One declaration: :root { color-scheme: light dark; }. Both values rather than one per media query — that declares support for both and lets the browser follow the visitor's own preference, which is exactly the system-only behaviour ADR-032 keeps. A single value would override the choice they already made in their OS.

Measurement corrected the ADR, and the correction is in it

ADR-032 §2 originally listed an <input>'s default background and a number input's spin buttons among what this fixes, labelled as reasoning from the spec rather than an observation. The observation contradicted it.

Comparing a bare <input>, <select> and <textarea> inheriting light dark against the same three forced to light, with dark-scheme emulation on: computed background, colour and border were identical in both cases. Tailwind's Preflight already resets form controls to a transparent background and inherited colour, so that user-agent styling was never in play here. The claim was wrong for this codebase; the list in §2 is now shorter and honest, and says which items are and are not observable.

Two of my own test bugs, and the second is the one worth reading

The first assertion was /color-scheme\s*:/ — which also matches prefers-color-scheme:, so it would have passed with the declaration entirely absent. Its sibling, reading the value, matched that same media query and failed against a correct stylesheet; that honest failure is the only reason the silent one was found. Both now go through a (?<!prefers-) lookbehind, and the named constant carries the explanation.

A third, smaller: new RegExp(... + "\s*...") — "\s" in a JS string literal is just s, so the pattern meant "zero or more literal s" and worked by luck. Now String.raw.

Five assertions, each provoked

Provocation Fails
Remove the declaration 3 tests
Force a single scheme (dark) the "both schemes" test
Add a data-theme selector the test pinning ADR-032's no-toggle decision

That last one matters: if a toggle is ever built, that test is where the decision visibly changes rather than quietly eroding.

Verified, and not

Verified in a browser with dark-scheme emulation, against a rounded-full control so an unloaded stylesheet could not be mistaken for a fix: colorScheme on the root is light dark, prefers-color-scheme: dark matches, dark tokens are active (--color-ink: #f1f2fc), and the declaration reaches the built CSS.

Not verified, and §2 now says so: nobody has looked at a dark-mode scrollbar or an autofilled field. Neither is observable here — a scrollbar's colours are not exposed to getComputedStyle, and autofill needs a real interaction. That is roadmap A4, which no longer gates this fix but is now its confirmation. The autofill case matters most, because a browser-painted background can drop text below the 4.5:1 contrast.test.ts certifies — and that test cannot see a background the browser painted.

Status of the ADR

ADR-032 stays proposed for §3's toggle question alone; §2 is marked extracted and fixed. STATUS and the roadmap note that ADR-033 has the same shape and could be split the same way — its logical-properties and <track> halves are separable from §4's deferral of localisation.

421 frontend tests across 36 files, tsc, eslint, verify:css, verify:a11y, verify:static all pass.

🤖 Generated with Claude Code

…urniture

Split out of ADR-032 at the owner's instruction. That ADR asked a product question
— should the product have a manual theme toggle — and found a defect while asking.
The defect needed no product decision, so it should not have waited behind one.

`globals.css` swapped 17 tokens and two shadows inside a `prefers-color-scheme`
query, and that is everything *our* stylesheet paints. `color-scheme` was never
declared, so everything the browser paints itself stayed light: scrollbars,
autofill, native video controls, select popups, date pickers.

`light dark` rather than a value per media query. It declares support for both and
lets the browser follow the visitor's own preference, which is exactly the
system-only behaviour ADR-032 keeps — a single value would override the choice the
user already made in their operating system.

**Measurement corrected the ADR, and the correction is in it.** §2 originally
listed an `<input>`'s default background and a number input's spin buttons among
what this fixes. Comparing a bare `<input>`, `<select>` and `<textarea>` inheriting
`light dark` against the same three forced to `light`, with dark-scheme emulation
on: **computed background, colour and border were identical.** Tailwind's Preflight
already resets form controls to a transparent background and inherited colour, so
that user-agent styling was never in play. The claim was wrong for this codebase
and the list is now shorter and honest.

**Two of my own test bugs, and the second is the one worth reading.** The first
assertion was `/color-scheme\s*:/`, which also matches `prefers-color-scheme:` —
so **it would have passed with the declaration entirely absent.** Its sibling,
reading the value, matched the same media query and failed against a correct
stylesheet; that failure is the only reason the silent one was found. Both now go
through a `(?<!prefers-)` lookbehind, and the constant carries the story. A third:
`new RegExp(... + "\s*...")` — `"\s"` in a JS string literal is just `s`, so the
pattern meant "zero or more literal s" and worked by luck. Now `String.raw`.

Five assertions, each provoked: removing the declaration fails three, forcing a
single scheme fails one, and adding a `data-theme` selector fails the one that
pins ADR-032's no-toggle decision — so if a toggle is ever built, that test is
where the decision visibly changes.

**Verified in a browser** with dark-scheme emulation, against a `rounded-full`
control so an unloaded stylesheet could not be mistaken for a result:
`getComputedStyle(document.documentElement).colorScheme` is `light dark`,
`prefers-color-scheme: dark` matches, dark tokens active, and the declaration
reaches the built CSS.

**Not verified, and §2 now says so:** nobody has looked at a dark-mode scrollbar
or an autofilled field. Neither is observable here — a scrollbar's colours are not
exposed to `getComputedStyle` and autofill needs a real interaction. That is
roadmap A4, which no longer gates this fix but is now its confirmation.

ADR-032 stays `proposed` for §3's toggle question alone. STATUS and the roadmap
note that **ADR-033 has the same shape** and could be split the same way: its
logical-properties and `<track>` halves are separable from §4's deferral.

421 frontend tests across 36 files, tsc, eslint, verify:css, verify:a11y and
verify:static all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khafifithebork
khafifithebork merged commit e3358c0 into master Sep 27, 2026
8 checks 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