xoong
Menu · Focus audit
DocsDesignFocus audit

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):

PaletteRingpagesurfacesubtleaccent-softinverse
Calm · light#9a4d335.49 5.93 4.94 4.97 2.48 fail
Calm · dark#eeaa909.36 8.62 7.82 7.71 6.21
Ember · light#b93a0a5.42 5.72 5.06 4.68 3.15
Ember · dark#ff90688.44 7.81 7.08 6.81 5.43
Framework default · light#2457d65.80 6.16 5.16 4.88 2.69 fail
Framework default · dark#7aa2ff7.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).

page · light
surface · light
subtle · light
accent-soft · light
inverse · light
page · dark
surface · dark
subtle · dark
accent-soft · dark
inverse · dark

Rules the design follows

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).

ControlTab stopsKeys
xo-tabs1 (the selected tab)← → move, Home/End first/last, panels are not tab stops until focused
xo-menu1 (the trigger)Enter/Space/↓ open, ↑↓ Home End move, a letter jumps, Esc closes and returns, Tab closes
xo-combobox / xo-tags1 (the input)↓↑ open and move (focus stays in the input), Enter picks, Esc closes, Backspace removes the last chip
xo-carousel1 per button + the tracktrack: ← → Home End; buttons: Enter/Space; hidden slides are inert
xo-richtext1 toolbar + 1 editortoolbar: ← → Home End (roving); Ctrl/Cmd+B I U K in the editor
xo-tableeach header button, each row controlEnter/Space on a header sorts; the select-all checkbox first
xo-dialog / confirmtrapped inside while openEsc closes and focus returns to the opener
Skip link1 (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