Accessibility
How SubstrateUI approaches accessibility, what the system handles for you, and what you are still responsible for when you build on top of it.
Philosophy
Accessibility is a baseline requirement for SubstrateUI, not an optional feature or a follow-up pass. Every primitive in the library is expected to be usable by keyboard, legible at WCAG AA contrast, and respectful of user motion preferences the moment you drop it into an application. The goal is that building accessibly should be the path of least resistance — you should have to go out of your way to produce an inaccessible interface.
That said, a design system cannot make an application accessible on its own. Accessibility is a property of the whole experience: the components, the content they wrap, the structure of each page, and the way keyboard focus moves through a flow. SubstrateUI gives you a solid floor. The ceiling is up to you.
What SubstrateUI Provides
Interactive components are built on Radix UI primitives, which supply focus management, correct ARIA roles and states, keyboard navigation, and screen reader announcements out of the box. Overlays trap focus while open and restore it to the trigger on close. Menus and comboboxes follow the ARIA authoring practices for their respective patterns.
Every token pairing in the color system is verified against WCAG AA contrast thresholds (4.5:1 for normal text, 3:1 for large text and UI elements) in both light and dark mode. The audit runs as part of the library build, so a regression in any token relationship fails the release. The current report is embedded in these docs at the contrast matrix page.
Motion respects the user's system preference. A global prefers-reduced-motion rule disables transitions, animations, and smooth scrolling when the operating system requests reduced motion, and individual components (notably the Button press-down) ship their own motion-reduce overrides so they remain static rather than jumpy.
Semantic HTML is used wherever the primitive has a natural element counterpart: Fieldset and Legend render the real tags, Kbd renders <kbd>, FieldError uses role="alert" so validation errors are announced as they appear, and every focusable element has a visible focus ring with at least a 3:1 contrast against its background.
What You Are Still Responsible For
Icon-only buttons need an aria-label. The library cannot guess what a lone <Plus /> icon is supposed to do — you have to tell it. The same applies to any interactive control whose meaning is communicated only through an icon or a color.
Form fields need labels. Use Field with FieldLabel so the label is programmatically associated with its input; placeholder text is not a substitute, because it disappears the moment the user starts typing and usually fails contrast requirements.
Heading hierarchy belongs to your pages, not to the system. Use one H1 per page, follow it with H2, and don't skip levels to chase a visual size. Screen reader users navigate by heading structure, and a scrambled outline makes a page hard to understand even when every individual component is perfect.
Finally, test with assistive technology. Run through the app with the keyboard only. Turn on VoiceOver or NVDA and listen to what your flows actually sound like. No automated audit can replace hearing your own interface read aloud.
Testing Your App
The minimum viable accessibility pass on any new screen is a keyboard-only walkthrough: tab through every interactive element, make sure focus is always visible, confirm that Enter and Space activate controls appropriately, and verify that Escape closes overlays and returns focus to the trigger.
Beyond that, test with a screen reader — VoiceOver on macOS and iOS, NVDA on Windows, TalkBack on Android — and listen for missing labels, unannounced state changes, and reading order that doesn't match the visual order. Zoom the browser to 200% and confirm the layout still works. Enable the system reduced-motion preference and watch for components that keep animating anyway. Toggle dark mode and make sure nothing you've built depends on a light-mode-only assumption.
Touch Targets
WCAG 2.5.5 recommends a minimum touch target of 44×44 CSS pixels. The default Button height in SubstrateUI is 40px, which is comfortable on a desktop pointer-driven interface but sits just below the WCAG target on touch. For mobile-primary contexts — the key action in a sheet, the submit of a mobile form, a tap target surrounded by empty space on a small screen — prefer size="lg", which is 44px and meets the guideline.
The same principle applies to icon-only buttons and any custom interactive element you build: give touch users enough room to land their thumb without misfiring on a neighbor.
Contrast Matrix
The full contrast audit — every token pairing, measured in both light and dark mode against the WCAG AA threshold for its usage type — is published at the contrast matrix page. If you extend the token system with custom colors, add your pairings to the audit script so the same guarantee extends to them.
Known Limitations
SubstrateUI does not yet ship automated screen reader testing or ESLint rules for accessibility. Contribution is gated by the contrast audit and a manual review checklist (see CONTRIBUTING.md), and a11y regressions should be caught in review rather than by tooling. Adding axe-core to the documentation build and eslint-plugin-jsx-a11y to the lint config are reasonable next steps for consuming applications.