Getting started

Accessibility

Nova treats accessibility as a design constraint, not a compliance task. Every component ships with an accessibility contract we test against — not an aspirational note.

Baseline commitments

Every stable component meets or exceeds WCAG 2.2 AA. Interactive primitives are built on Radix UI where semantics matter, and every custom widget is verified with axe-core in CI.

  • Color contrast ≥ 4.5:1 for body text, 3:1 for large text and UI.
  • Every icon-only control has an aria-label. IconButton enforces it at the type level.
  • Form inputs always have a semantic <label>, not a placeholder-as-label.
  • Live regions announce async status (loading, saved, error).
  • Every overlay traps focus, restores it on close, and closes on Escape.
  • Every primitive is reachable with a keyboard alone — no mouse-only affordances.

Keyboard navigation

Tab moves between controls, arrow keys move within composite widgets, Enter/Space activates, and Escape closes overlays. Focus is trapped in modals and restored on close.

KeyBehavior
Tab / Shift+TabMove to next / previous focusable control
EnterActivate buttons, submit forms
SpaceActivate buttons, toggle checkboxes
Arrow keysNavigate within radio groups, menus, tabs, sliders
EscapeClose overlays, cancel edits
Home / EndJump to first / last item in a list or slider

ARIA patterns

Nova follows the WAI-ARIA Authoring Practices for every composite widget. When Radix provides a primitive with the right pattern, we use it — reinventing ARIA state machines is where accessibility bugs are born.

  • Dialogs use role="dialog" with aria-modal="true" and aria-labelledby pointing at the title.
  • Menus use role="menu" with roving tabindex, arrow-key navigation, and typeahead.
  • Tabs use role="tablist"/"tab"/"tabpanel" with aria-selected and aria-controls wired in.
  • Disclosure patterns use aria-expanded and aria-controls on the trigger.
  • Toasts use role="status" for informational and role="alert" for urgent.

Reduced motion

Nova respects prefers-reduced-motion. Decorative motion (aurora drifts, shimmers) is held. Meaningful motion (spinners, progress) is preserved but slowed.

css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}

Focus visibility

Focus rings are two-layer — an inner ring in the background color, an outer ring in the accent — so focus stays visible on any surface, light or dark. Never suppress :focus-visible in an app that uses Nova.

Color & contrast

Nova's OKLCH token system is contrast-audited on both themes. Semantic pairs (fg on bg, muted-fg on subtle, accent on bg) meet WCAG AA at every combination we ship. If you author a custom theme, run the CLI's contrast checker before shipping.

Forms & error messaging

Every input needs a <Label>. Error messages are linked with aria-describedby and the input carries aria-invalid="true" when the field fails validation.

tsx
<Label htmlFor="email">Email</Label>
<Input
id="email"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && (
<p id="email-error" role="alert" className="text-danger text-sm">
Enter a valid email address.
</p>
)}

Screen readers

Nova is verified against VoiceOver (macOS/iOS), NVDA (Windows), and TalkBack (Android). Live regions are used sparingly — only for status the user needs to hear without shifting focus.

Testing checklist

  • Navigate the entire flow with a keyboard only. Every action must be reachable.
  • Run axe-core (or Lighthouse) against the page — zero violations before merging.
  • Enable prefers-reduced-motion in the OS and verify decorative motion is held.
  • Zoom to 200% — no clipped content, no horizontal scroll on standard breakpoints.
  • Test with a screen reader: does every button announce its purpose without visual context?