Proper UI

Accessibility

What Proper UI's accessibility testing actually covers: automated axe-core checks in CI, what React Aria provides, and what composition still requires human review.

The claim, precisely

Every component group in packages/ui/src/components ships a test suite that renders each documented variant and asserts zero detected axe-core violations. As of this page, that's 118 test suites, all 118 asserting toHaveNoViolations, running on every pull request in CI.

That number is real, and it's also narrow. The W3C is direct about the ceiling: "Tools cannot check all accessibility aspects automatically. Human judgement is required." Automated tools "can not determine accessibility, they can only assist in doing so." This page exists so the number links to its own boundary instead of standing in for a claim the tests don't make.

What is automated

Each suite lives next to its component (button.test.tsx, modals.test.tsx, table.test.tsx, and so on) and follows the same shape: render every demo export, run axe(container) from vitest-axe inside jsdom, and assert the result toHaveNoViolations(). packages/ui/vitest.setup.ts wires the matcher globally and patches in the DOM APIs jsdom lacks (matchMedia, ResizeObserver, IntersectionObserver) so components that measure their own layout don't crash — it does not, and cannot, patch in a real rendering engine.

This runs on every PR, so a regression that introduces a detectable violation fails CI before merge. Concretely, axe-core in this setup checks things like:

  • Every interactive element has an accessible name.
  • ARIA attributes are valid, on the correct role, and don't contradict each other.
  • Form fields have programmatically associated labels.
  • Heading levels and landmark roles present in the rendered markup are structured correctly relative to each other.
  • Roles that require specific children or parents (menus, listboxes, grids) have them.

What it does not, and structurally cannot, check:

  • Meaningful alt text. Axe confirms an <img> has an alt attribute; it has no way to judge whether alt="image" is worse than no image at all.
  • Focus order sensibility. It can confirm elements are focusable and in the accessibility tree; whether tabbing through a page visits things in an order a person would consider sane is not something a DOM snapshot answers.
  • Colour contrast, in this setup specifically. Axe's contrast checks work by rendering; jsdom does no layout or paint, so there is no computed style, no getBoundingClientRect, and no way for axe to sample a background against a foreground. The rule isn't disabled, it simply never has anything to measure in this test environment.
  • Screen-reader announcement quality. Correct ARIA (roles, states, aria-live) doesn't guarantee an announcement makes sense out loud, in the order it fires, at the verbosity a screen-reader user wants.
  • Keyboard operability beyond static ARIA. Axe reads the DOM as rendered; it doesn't press keys. Of the 118 suites, only header-navigations.test.tsx simulates a keyboard interaction (opening a menu, then dismissing it with Escape, via fireEvent.keyDown). Every other suite verifies the ARIA is correct in a single static render — not that arrow keys, Home/End, typeahead, or focus trapping behave correctly when actually driven.

What React Aria provides

The keyboard and screen-reader behaviour the axe suites don't test for is largely the job of a different layer: React Aria Components, which every interactive Proper UI component (menus, dialogs, comboboxes, tables, date pickers, sliders) is built on rather than reimplementing from scratch.

React Aria's own documentation is specific about what it takes on:

  • Keyboard navigation. "Arrow key navigation, typeahead, multiple selection modifiers, landmark navigation, and much more" — keyboard interactions are described as first-class, not an add-on.
  • Focus management. "Focus is automatically contained within overlays, restored on close, and moved when list items are deleted. Focus rings appear only when using the keyboard."
  • ARIA wiring. Components "implement semantics and keyboard behavior according to the W3C ARIA Authoring Practices Guide," with React Aria adding "real-world testing and device support" on top of the spec.
  • Screen-reader behaviour. React Aria states its behaviors "work without a keyboard, ensuring touch screen reader users have full access," and that components are "extensively tested using many popular screen readers and devices" — testing this project's own suites don't reproduce.
  • Internationalization and RTL. Translations in 30+ languages, localized date/number formatting across 13 calendar systems, and right-to-left layout support are built in — see the RTL support page for how far Proper UI's own styling layer has followed that support.

In short: the axe suites verify the ARIA output is well-formed at render time; React Aria is the reason there's correct keyboard and screen-reader behaviour underneath it for the suites to render in the first place. Neither one substitutes for the other, and neither one is a claim of "fully accessible."

What is on you

React Aria gives every primitive correct interaction behaviour, and the axe suites catch the class of markup mistakes that are mechanically detectable. Neither one reviews how you compose those primitives into a page. Specifically still your responsibility:

  • Labels for icon-only controls. A Button with only an icon child needs an explicit aria-label — nothing in the component can infer what the icon means.
  • Contrast of custom brand colours. The shipped tokens in theme.css meet WCAG 2.1 AA in both themes; if you override a brand colour, you own re-checking contrast, since jsdom cannot verify it for you (see above) and nothing else in the pipeline will.
  • Heading order and landmark structure across a composed page. A single component's demo can be internally well-structured while the page you build from several of them skips a heading level or nests two <main> landmarks. Axe only ever sees one rendered tree at a time — the composition is yours to check.
  • Alt text for the images you add. The library doesn't ship your images or copy.
  • Motion preferences. Components with transitions don't unilaterally decide what prefers-reduced-motion should do in your product.

This applies whether a human or an AI assistant is writing the composition. An assistant generating a page from these primitives can still put a heading level out of order, nest landmarks incorrectly, or leave an icon button unlabeled — the primitives being sound doesn't make the assembly automatically sound. Generated or hand-written, page composition is something that still gets reviewed, not something the 118 green suites already cover.

Manual verification matrix

No manual assistive-technology audit has been performed on these components yet. The table below is the honest state of that work — every cell is unverified, not passing:

ComponentKeyboardVoiceOver / macOSNVDA / WindowsTouch
Menus / dropdownsNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
Modals / dialogsNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
Comboboxes / selectsNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
Date pickersNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
TablesNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
TabsNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
SlidersNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified
File uploadNot yet verifiedNot yet verifiedNot yet verifiedNot yet verified

This matrix will be filled in, component by component, as real manual verification happens — not as a one-time pass, since a regression can reopen a cell that was once checked. If you've run one of these components through an actual screen reader or keyboard-only pass, open an issue or a pull request with what you found; contributions here are the fastest way this table stops being all "not yet verified."

The wording rule

Every mention of this testing across the site, marketing included, follows one rule: say "0 detected axe violations in 118 automated suites," never "fully accessible." The first sentence is a fact about a specific, named test. The second is a claim no automated suite, and no library, gets to make on your behalf.

Known limitations

  • RTL is infrastructure-complete, styling-incomplete. React Aria's direction handling (arrow-key navigation, popover placement, field order) is done. The Tailwind logical-properties migration in packages/ui/src/components is not — see RTL support for the current counts. A component that hasn't migrated will not mirror correctly in a dir="rtl" document.
  • No contrast testing. As noted above, jsdom performs no layout, so axe's colour-contrast rule has nothing to sample against in this test environment. Contrast is currently guaranteed only by the shipped token values, not by an automated check.
  • 118 suites test render output, not interaction, almost everywhere. Aside from the one keyboard-driven suite noted earlier, "no violations" describes a single static render of each demo — not a full session of tabbing, arrowing, and dismissing through it.
  • No visual regression baseline. Per the project roadmap, the screenshot tooling exists but has no committed baseline and no CI job, so a visual accessibility regression (e.g., a focus ring silently removed) would not be caught by any automated check today.

FAQs