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.
| Key | Behavior |
|---|---|
| Tab / Shift+Tab | Move to next / previous focusable control |
| Enter | Activate buttons, submit forms |
| Space | Activate buttons, toggle checkboxes |
| Arrow keys | Navigate within radio groups, menus, tabs, sliders |
| Escape | Close overlays, cancel edits |
| Home / End | Jump 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.
@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.
<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?