Date: 2026-07-30 Type: phase-update
The full pages-theme-picker renders a select dropdown plus light/dark toggle buttons — fine for a settings panel, wrong for a toolbar. Issue #245 asked for a single icon button that opens a flyout popover with the same controls. The hard parts looked like positioning, click-outside dismissal, focus management, and a11y.
I expected to pull in FocusTrapMixin from pages-primitives, write a click-outside handler that respects shadow DOM boundaries (a known pain point — GE-20260713-9e6bf5 documents the composedPath() limitation in jsdom), and manage z-index stacking manually. I’d seen enough custom popover implementations to know the pattern: 50-100 lines of JS for positioning, another 30 for dismiss behaviour, a few more for focus restoration.
Before writing any implementation, I searched for current platform capabilities. Two Baseline standards changed the equation:
The Popover API (Baseline 2025) gives popover="auto" on any element: light-dismiss, Escape-to-close, auto aria-expanded on the invoker, top-layer rendering, and tab-order repositioning. All browser-native, zero JS.
CSS Anchor Positioning (Baseline 2026) handles placement: anchor-name on the trigger, position-anchor plus position-area on the popover, position-try-fallbacks: flip-block for viewport edge flipping. Declarative CSS, no Floating UI.
The key question was whether popovertarget (which resolves by ID) would work inside a shadow root. The whatwg spec issue #9109 discusses cross-shadow-boundary limitations, and reading that alone would make you think it won’t work. It does — both elements are in the same shadow root, which is the same tree for ID resolution purposes. That distinction isn’t documented in any Lit or web component guide I could find.
The entire popover — open, close, dismiss, position, focus, a11y — is handled by the browser. We wrote CSS and markup. The JS footprint is the three event handlers that were already there: family change, mode toggle, and theme application.
One design decision worth noting: the compact popover renders radio buttons when there are 5 or fewer theme families, and switches to a <select> dropdown above that threshold. With 2-3 families (the common case), radios give a better at-a-glance experience. With many families, they’d make the popover uncomfortably tall.
Single-family deployments get an even simpler popover — just the light/dark toggle, no family section at all.
The session started with CI broken on main — 17 ESLint strict-type-checked errors and 3 test failures, all pre-existing. The lint errors were mechanical: unused imports, floating promises, unnecessary type conversions, an as any that should have been a branded type cast. The test failures were a stale chroma threshold in the CaseHub preset test and two theme class assertions using the old naming convention (pages-theme-dark instead of pages-theme-default-dark).
None were related to #245, but CI had been red for days with every run skipping tests because lint failed first. We fixed them on main before starting the feature branch.
The compact picker works — palette icon with accent dot, popover with family radios and mode toggle, all positioning and dismiss handled by the browser. The anchor-scope: all on :host prevents anchor name collisions when multiple pickers exist on the same page, which was a gotcha worth recording (GE-20260730-ec4b06).
The Popover API pattern probably has legs beyond the theme picker. Any component that needs a dropdown or flyout — filter chips, column pickers, context menus — could use the same approach. The entire Floating UI dependency category becomes optional when the browser provides positioning, dismiss, and top-layer rendering natively.