Skip to content
Documentation menu
WCAG 2.2 AA

Accessibility contract

Lumora's accessibility is part of the public API. Components ship keyboard states, ARIA semantics, focus rings, and AA-tested contrast — verified in CI on every release.

The promise

Every Lumora primitive is designed around five hard constraints. If a release would break any of them, we don't ship it.

Promise 1Keyboard access
Promise 2Visible focus states
Promise 3Programmatic names and states
Promise 4Color contrast
Promise 5Reduced motion
Promise 6Error identification

Verified in CI

Two automated suites run on every commit and block merges if they fail.

Suite 1Theme contrast

Iterates every theme and asserts WCAG AA contrast ratio (4.5:1) on 10 critical foreground/background pairs — surface/text, primary/primary-fg, every semantic pair.

tests/themes.test.ts

Suite 2Component contracts

Asserts that every documented selector, ARIA-state class, and component contract survives a release. Snapshots primary controls so styling regressions show up as diffs.

tests/core.test.ts

Run it yourself

Clone the repo and run pnpm test — both suites finish in under 2 seconds. Use them to validate custom themes too.

Component patterns

Each component below ships specific keyboard, ARIA, and focus contracts. Treat this as the audit reference for your screen-reader and keyboard testing.

Button

role=Native button or link with button-like styling
2.1.1 Keyboard2.4.7 Focus Visible4.1.2 Name, Role, Value
Required ARIA
  • type on form buttons
  • aria-busy when loading
  • aria-disabled only when non-native
Keyboard
  • Enter activates
  • Space activates when rendered as button
Focus & notes

Use the built-in focus-visible ring and never remove focus outlines without replacement.

Use native disabled for button elements; reserve aria-disabled for links or custom elements.

Form controls

role=Native input, select, textarea, checkbox, radio, or switch
1.3.1 Info and Relationships3.3.1 Error Identification3.3.2 Labels
Required ARIA
  • label or aria-label
  • aria-invalid for errors
  • aria-describedby for hints/errors
Keyboard
  • Tab moves between fields
  • Space toggles checkbox, radio, and switch
Focus & notes

Focus stays on the edited field; validation messages must be announced through descriptions.

Do not rely on color alone for invalid, warning, or success states.

Tabs

role=tablist, tab, and tabpanel
2.1.1 Keyboard2.4.3 Focus Order4.1.2 Name, Role, Value
Required ARIA
  • aria-selected
  • aria-controls
  • aria-labelledby
  • tabindex
Keyboard
  • Arrow keys move between tabs
  • Home moves to first tab
  • End moves to last tab
Focus & notes

Focus should move to the active tab; tab panels should not trap focus.

Use roving tabindex for interactive tab lists.

Dropdown

role=button plus menu/menuitem when used as an action menu
2.1.1 Keyboard2.4.3 Focus Order4.1.2 Name, Role, Value
Required ARIA
  • aria-haspopup
  • aria-expanded
  • aria-controls
  • role=menuitem
Keyboard
  • Enter or Space opens
  • Escape closes
  • Arrow keys move through menu items
Focus & notes

Move focus into the menu on open and return focus to the trigger on close.

Use aria-disabled on disabled menu items and keep them focusable only when the pattern requires it.

Modal

role=dialog
2.1.1 Keyboard2.4.3 Focus Order2.4.7 Focus Visible
Required ARIA
  • aria-modal=true
  • aria-labelledby or aria-label
Keyboard
  • Escape closes when safe
  • Tab cycles inside the dialog
  • Shift+Tab cycles backward
Focus & notes

Move focus to the first meaningful control on open and restore focus to the trigger on close.

Background content should be inert while the modal is open.

Drawer

role=dialog or complementary depending on behavior
2.1.1 Keyboard2.4.3 Focus Order4.1.2 Name, Role, Value
Required ARIA
  • aria-modal=true for blocking drawers
  • aria-labelledby or aria-label
Keyboard
  • Escape closes blocking drawers
  • Tab order remains contained when modal
Focus & notes

Blocking drawers follow modal focus rules; persistent drawers must not trap focus.

Choose drawer semantics based on whether it blocks interaction with the page.

Tooltip

role=tooltip
1.4.13 Content on Hover or Focus2.1.1 Keyboard
Required ARIA
  • aria-describedby on trigger
  • role=tooltip on content
Keyboard
  • Appears on focus
  • Escape dismisses when interactive dismissal is implemented
Focus & notes

Tooltip content does not receive focus unless it contains interactive content.

Tooltips must be dismissible, hoverable, and persistent enough to read.

Toast

role=status or alert
4.1.3 Status Messages2.2.1 Timing Adjustable
Required ARIA
  • role=status for passive updates
  • role=alert for urgent errors
Keyboard
  • Dismiss button is keyboard reachable when present
Focus & notes

Do not steal focus for passive notifications.

Use polite announcements for success/info and assertive only for urgent failures.

Table

role=Native table
1.3.1 Info and Relationships2.4.6 Headings and Labels
Required ARIA
  • caption or accessible name for complex tables
  • scope on header cells
Keyboard
  • Native reading and browser table navigation should remain intact
Focus & notes

Interactive cells must have visible focus and predictable tab order.

Prefer native table markup for enterprise data grids until richer grid behavior is implemented.

Audit checklist

Run through this list when shipping a feature built on Lumora. Most items are already handled by the components — this is the residual surface area in your application code.

  • Every interactive element receives keyboard focus and shows the Lumora focus ring.
  • All form inputs have associated <label> elements (or aria-label).
  • Hint text and error messages are linked via aria-describedby.
  • Toast and live-update regions wrap content in role='status' or aria-live='polite'.
  • Modals and drawers trap focus and restore it to the trigger on close.
  • Color is never the only signal for state — pair with text or icon.
  • Skip-link is the first focusable element on every page.
  • Animations honor prefers-reduced-motion (Lumora handles this for shipped components).
  • Heading order is not skipped — every page starts with a single <h1>.