Luthor targets WCAG 2.1 Level AA. This page records what is implemented and verified, so hosts can cite it in their own accessibility statements — and what is not, so nobody cites something that isn't true.
| Surface | Keys |
|---|---|
| Editor | Standard text editing. Tab inserts indentation inside lists and code; press Escape first to move focus out of the editor instead. |
| Toolbar | Tab to reach it, arrow keys within grouped controls, Enter/Space to activate. |
Slash menu (/) | ↑/↓ to move, Enter to insert, Escape to dismiss. Focus stays in the editor. |
Emoji menu (:) | Same as the slash menu. |
| Command palette | Escape to dismiss, ↑/↓ to move, Enter to run. Focus is in the search input. |
| Link bubble | Reachable by Tab; Escape returns to the editor. |
A rich-text editor that swallows Tab traps keyboard users. Luthor's
TabIndentExtension takes Tab for indentation, but pressing Escape
first releases it: the next Tab then moves focus out of the editor
normally. Every other focus-capturing surface follows the same rule —
Escape always returns control to the caret.
The three typeahead surfaces deliberately keep DOM focus in the editor while open, because moving focus would interrupt typing. They therefore expose the ARIA combobox/listbox relationship instead:
role="listbox" with an aria-label,role="option" carrying aria-selected,aria-activedescendant on the container (or, for the palette, on its
search input) names the active option, so arrowing through the list is
announced without focus moving,tabIndex={-1}, keeping them out of the Tab order,role="status".The command palette is additionally a role="dialog" with
aria-modal="true", and its input is a role="combobox" wired to the
list via aria-controls. The link bubble is a labelled role="group"
so its controls are announced in context, and its URL field sets
aria-invalid when the value is rejected.
Purely decorative chrome — menu headers, group titles that duplicate an
aria-label, emoji glyphs beside their own text label — is marked
aria-hidden so it is not read twice.
Luthor honors prefers-reduced-motion: reduce. Most of its motion flows
through the --luthor-theme-transition token, which the query sets to
0s; animations and transitions that do not read the token are reduced
to a negligible duration by the same block. Hosts overriding Luthor's
CSS should keep that media query in place.
packages/luthor/src/core/accessibility.test.tsx guards the semantics
above: listbox/option roles, aria-selected, aria-activedescendant
wiring per surface, dialog and combobox attributes on the palette,
labelled link-bubble controls, and the reduced-motion block including
its token reset.
These are honest limits, not oversights to be discovered later.
The a11y tests assert on component source, not on rendered output, because the overlay surfaces only mount behind editor state that jsdom cannot produce (a live caret with a real selection rectangle). A render-level audit — and any real screen-reader verification with NVDA, JAWS, or VoiceOver — needs a browser-driven suite. Until that exists, treat this page as describing implemented semantics rather than observed assistive-technology behaviour.