Focus audit
What the keyboard focus ring looks like on every surface, in Calm and Ember, light and dark, and the result of tabbing through the real pages. axe cannot check this; the rules are WCAG 2.4.7 (visible) and 1.4.11 (3:1 against what surrounds it).
Result
2116 focus states, 0 failures after the fixes below: 12 pages (components, forms, data, demo, carousel, menu, tabs, combobox, rich text, own control, styling, overlays) x Calm/Ember x light/dark, tabbing through every control.
Before the fixes: menu items (and the rich-text More menu) signalled focus with a fill change only, about 1.3:1, in all four combinations. They now also get an inset 2px ring.
Ring contrast per surface
The ring is --xo-color-accent, drawn 2px outside the control (outline-offset: 2px), so it sits on whatever surrounds the control. Contrast of the accent against each surface token (needs 3:1):
| Palette | Ring | page | surface | subtle | accent-soft | inverse |
|---|---|---|---|---|---|---|
| Calm · light | #9a4d33 | 5.49 | 5.93 | 4.94 | 4.97 | 2.48 fail |
| Calm · dark | #eeaa90 | 9.36 | 8.62 | 7.82 | 7.71 | 6.21 |
| Ember · light | #b93a0a | 5.42 | 5.72 | 5.06 | 4.68 | 3.15 |
| Ember · dark | #ff9068 | 8.44 | 7.81 | 7.08 | 6.81 | 5.43 |
| Framework default · light | #2457d6 | 5.80 | 6.16 | 5.16 | 4.88 | 2.69 fail |
| Framework default · dark | #7aa2ff | 7.28 | 6.56 | 6.17 | 5.60 | 4.26 |
Framework default rows use the neutral defaults you gave me (accent #2457d6 / #7aa2ff on #f7f8fa / #14161a); subtle, accent-soft and inverse for them are my derived estimates (6-7% text, 12-16% accent, #1b1f24 / #3a3f47).
One exception: inverse surfaces in the light scheme. The accent is 2.48:1 on Calm's inverse and 2.69:1 on the framework default. Anything focusable on a toast or tooltip uses --xo-focus-inverse (3px, the inverse text colour, 13:1+). Ember light passes at 3.15:1 but gets the same treatment for consistency.
Focus rings, forced
Buttons, a primary button, a field, an icon button and a tab in the focus state, on each surface, light then dark (use the palette menu for Ember).
Rules the design follows
- One ring:
--xo-focus: 3px solid var(--xo-color-accent), 2px offset. Never remove it; fields also change border to the accent and add a soft glow. - List items (menu, rich-text More): fill change plus a 2px inset ring (
--xo-focus-thin). - On inverse surfaces:
--xo-focus-inverse. - The ring colour is the accent text colour (
--ember-deep), never the pot colour (--ember, 2.4:1 on Paper). - Composite controls draw the ring once, on the wrapper (
:focus-within): combobox box, rich-text box. No double rings.
Tab stops and keys
The order is the DOM order. Composite controls are one tab stop and move inside with the arrow keys (roving tabindex).
| Control | Tab stops | Keys |
|---|---|---|
| xo-tabs | 1 (the selected tab) | ← → move, Home/End first/last, panels are not tab stops until focused |
| xo-menu | 1 (the trigger) | Enter/Space/↓ open, ↑↓ Home End move, a letter jumps, Esc closes and returns, Tab closes |
| xo-combobox / xo-tags | 1 (the input) | ↓↑ open and move (focus stays in the input), Enter picks, Esc closes, Backspace removes the last chip |
| xo-carousel | 1 per button + the track | track: ← → Home End; buttons: Enter/Space; hidden slides are inert |
| xo-richtext | 1 toolbar + 1 editor | toolbar: ← → Home End (roving); Ctrl/Cmd+B I U K in the editor |
| xo-table | each header button, each row control | Enter/Space on a header sorts; the select-all checkbox first |
| xo-dialog / confirm | trapped inside while open | Esc closes and focus returns to the opener |
| Skip link | 1 (first) | Enter jumps to #main (add one to the page shell) |
Run it yourself
node design/brand-kit/audit/focus-check.mjs components forms data demo
KIT=1 node design/brand-kit/audit/focus-check.mjs ... # with the kit CSS laid over the built site
node design/brand-kit/audit/ring-contrast.mjs # the ring matrix above