new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

Supabase · Auth · all subjects

accessibility/keyboard-navigation

8 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Interactive elements must be keyboard-focusable

All interactive page elements should be reachable by keyboard. Add tabIndex={0} to all buttons, links, and non-text inputs, ideally at the component level. Consider tying the state of tabIndex to the disabled state of a component, if applicable.

Focus ring utilities for keyboard focus indicator

Use focus-ring for buttons, inputs, and most controls with an offset ring outline. Use focus-inset for dense or flush surfaces such as interactive table rows with an inset outline. The focus-ring utility expands to: outline-hidden, focus-visible:ring-2, focus-visible:ring-ring, focus-visible:ring-offset-2, focus-visible:ring-offset-background. The focus-inset utility expands to: outline: 2px solid transparent, outline-offset: -2px, with focus-visible setting outline-color and border-radius.

Focus indicator best practices

Prefer :focus-visible over :focus so click/tap does not show a focus indicator. Never use outline-none or outline-hidden without a ring or outline replacement. Always use the shared color (ring-ring or outline-ring); variants like primary, danger, or warning do not change focus colour. Do not animate the focus indicator; avoid transition-all or transition on controls. Prefer focus-ring or focus-inset over copy-pasting the class stack. On interactive table rows, use focus-inset only as focus-ring will look fine in some browsers and invisible in others.

Focus indicator on wrapper elements

When the focused element is not the thing that should show the ring (such as a wrapping Link with group, or an InputGroup parent using :has()), keep the explicit group-focus-visible:ring-* or has-[…]:focus-visible:ring-* stack. The utilities bake in :focus-visible on the same element and do not compose as group-focus-visible:focus-ring.

Built-in focus and tabIndex in components

Button, Checkbox, Switch, Select Trigger, Toggle, Accordion Trigger, Collapsible Trigger, Dropdown Menu Trigger, Popover Trigger, Dialog Trigger, Sheet Trigger, Alert Dialog Trigger, Sidebar Menu, and action buttons have focus, tabIndex, and the shared ring built-in. Bespoke interactive elements require these props to be added manually.

Interactive table row keyboard implementation

Interactive table rows require manual addition of tabIndex={0}, focus-inset class, onClick handler, and onKeyDown handler. The onKeyDown handler should check if the event.currentTarget equals event.target, and respond to Enter or Space keys by calling preventDefault and executing the navigation logic. The onClick handler should similarly check if event.currentTarget equals event.target before executing.

Radio group keyboard behavior

Single-select option groups using input type='radio' should behave as a single control. Only the first item of radio groups should become focused with the Tab key; the next Tab should move focus from the group to the next focusable control. Individual options inside the group can be reached by arrow keys (↑ ↓ ← →). Space is the canonical key to activate radio options, with Enter being a secondary affordance.

Keyboard navigation affordances for large content

For keyboard-navigable content with hundreds or thousands of items, help users jump to specific content with search and filtering, pagination or virtualization, and skip links and 'jump to' shortcuts. Apps with persistent header and sidebar chrome should expose a skip link as the first focusable element.

Give your agent this brain