One file, public/js/theme-picker.js, ES modules, no library and no build step, tying back to the Web Components Foundations course.
Progressive enhancement: with JavaScript off the control is simply absent and the page renders in the default theme.
One controller, many pickers
A module-level singleton from createController(): every dispatch runs reduce, save, the data-* attributes on <html>, then data-switching for the motion.
The theme-change event, bubbling and composed, fired from every connected instance so the popup and the /tokens/ page stay in sync.
No flash: the inline init script
An ES5 script under 700 characters in <head> that applies the stored attributes before first paint, generated from the same constants as the element.
The parity test that runs it under node:vm across 480 combinations, and the 12.25rem by 2.25rem box reserved so the nav does not move when the element upgrades.
Shadow DOM, tokens and the focus trap
Custom properties inherit across the shadow boundary, which is why a token system and web components fit together; adoptedStyleSheets only, which sets the browser floor (Safari 16.4+, Firefox 119+); older browsers keep the default theme and never upgrade the element.
The non-modal dialog: focus to the checked theme on open, Tab and Shift+Tab wrap with each radio group as one stop, Escape closes and refocuses, a composedPath check closes on outside clicks.
The panel that trailed the page
The bug: a transition on every shadow element made the panel settle about 600ms after a switch while the page settled at about 270ms.
The fix: registered --color-* properties already animate and inherit, so the extra transition now applies only where CSS.registerProperty is missing.
The 12,288-byte budget
The spec guessed about 250 lines; the element landed at 12,207 bytes and 216 lines, then 12,276 bytes after the carry-overs.
What was cut to stay inside it, including the mono face option removed end to end.
Directing the agents
What was prompted, what came back wrong (the CSS constant that shadowed the global, the panel running off screen at 360px), and what was corrected.
What Duncan would not accept, and why.
WRITING4 posts2 minblue mountains, australia · built 2026-10-26 20:40rss