Most component design systems treat accessibility as a checklist you run at the end. Add some ARIA labels, check the contrast ratios, test with a screen reader... done. Ship it.
That approach fails every time. Accessibility bolted onto finished components is fragile, incomplete, and the first thing that breaks when someone modifies a component. The only way to build an accessible design system is to make accessibility a structural decision, not a decoration.
Why Design Systems Are the Best Place to Solve Accessibility
Here's the practical argument: if your button component is accessible, every button on your site is accessible. If your modal component handles focus trapping correctly, every modal works for keyboard users. Fix it once in the system, and it's fixed everywhere.
The inverse is also true. If your design system's dropdown component doesn't support keyboard navigation, every dropdown on your site is broken for keyboard users. And you won't find out until someone files a complaint or you get an accessibility audit that flags 200 instances of the same problem.
According to the WebAIM Million annual analysis, which tests the top million homepages for accessibility, the most common failures are low contrast text, missing alt text, empty links, and missing form labels. Every one of those is a component-level problem. Fix them in the design system and they can't recur.
Keyboard Navigation: The Foundation Nobody Talks About
Keyboard accessibility is the unglamorous foundation of the whole thing. If a component can't be operated with a keyboard alone, it fails for screen reader users, switch device users, and power users who prefer keyboard navigation.
The ARIA Authoring Practices Guide (APG) defines keyboard interaction patterns for every common UI component. These aren't suggestions... they're the patterns users expect.
Key patterns your components need to implement:
- Tab order. Interactive elements should be reachable via Tab in a logical order. Non-interactive elements shouldn't be in the tab order at all. Use
tabindex="0"only when a custom element needs to be focusable; usetabindex="-1"for elements that should be programmatically focusable but not in the tab sequence. - Arrow key navigation within composite widgets. Tab grids, menu bars, radio groups, and tab lists should use arrow keys for internal navigation. The user Tabs into the component, then uses arrows to move between items within it. This is the roving tabindex pattern.
- Escape to dismiss. Modals, dropdowns, tooltips, and popovers should close on Escape. Always return focus to the element that triggered the overlay.
- Enter and Space to activate. Buttons respond to both. Links respond to Enter only. Custom interactive elements need to match the pattern of the native element they're replacing.
- Focus trapping. When a modal is open, Tab should cycle through the modal's interactive elements only. Focus should never escape behind the overlay.
Color Contrast: Beyond the Minimum
WCAG 2.1 Level AA requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18px bold or 24px regular). Level AAA raises those to 7:1 and 4.5:1 respectively.
But here's what the spec doesn't tell you... meeting the minimum ratio doesn't mean your text is easy to read. A 4.5:1 ratio with a busy background image behind the text is technically compliant and practically illegible.
For your design system's color tokens, build contrast checking into the token definition process:
- Define text/background pairs, not individual colors. "Primary text on white" is a pair. Both values need to exist together and be tested together.
- Test every pair against both AA and AAA standards. Aim for AAA where possible; use AA as the floor.
- Include focus indicator contrast. The focus ring around interactive elements needs to be visible against both the element background and the page background. WCAG 2.2 added Success Criterion 2.4.11 for focus appearance, requiring a minimum area and contrast for focus indicators.
- Don't rely on color alone to convey information. Error states should include an icon or text label, not just a red border. Status indicators should use shape or text in addition to color.
ARIA: Use It Correctly or Don't Use It at All
The first rule of ARIA is: don't use ARIA if a native HTML element does the job. A <button> element is always better than a <div role="button"> because the native element comes with keyboard behavior, focus management, and screen reader announcements built in.
When you do need ARIA (and you will, for custom components), these are the patterns that matter most in a design system:
aria-labelandaria-labelledby-- for elements that don't have visible text labels. Icon buttons, close buttons, navigation landmarks.aria-expanded-- for toggles that show/hide content. Accordions, dropdown menus, expandable sections. Must update dynamically when the state changes.aria-liveregions -- for content that updates dynamically. Toast notifications, form validation messages, loading states. Setaria-live="polite"for non-urgent updates andaria-live="assertive"for critical alerts.roleattributes -- only when you're building a custom widget that has no native HTML equivalent. Tabs (role="tablist",role="tab",role="tabpanel"), tree views, combo boxes.aria-describedby-- for supplementary information. Error messages on form fields, help text, constraints like character limits.
The mistake we see most often: developers adding ARIA attributes without testing with a screen reader. An incorrect role attribute is worse than no role at all because it gives screen readers wrong information about what an element is and how to interact with it.
Testing Accessibility in a Design System
Automated testing catches about 30-40% of accessibility issues. That's a meaningful chunk, and it should be part of your CI pipeline. But it misses the things that require human judgment... whether the tab order is logical, whether the screen reader announcement makes sense in context, whether the component is actually usable with a keyboard.
A practical testing strategy for a component library:
- Automated linting. Run eslint-plugin-jsx-a11y on every component. This catches missing alt text, incorrect ARIA usage, and non-interactive elements with click handlers.
- Automated testing. Use axe-core (via jest-axe or cypress-axe) to run WCAG checks against rendered component output. Include this in your component test suite so it runs on every PR.
- Keyboard testing. Tab through every interactive component manually. Verify the focus order, the keyboard interactions, and the focus visibility. This takes minutes per component and catches critical issues.
- Screen reader testing. Test with at least two screen readers... VoiceOver on Mac and NVDA on Windows cover the majority of screen reader users. Verify that every component announces its role, state, and label correctly.
- Storybook integration. If you're using Storybook, the a11y addon runs axe-core checks on every story and displays violations inline. This puts accessibility feedback directly in front of developers during component development.
Building Accessibility Into Your Component API
The goal is making it harder to build an inaccessible implementation than an accessible one. Here's how that works in practice:
- Make labels required props. If a component needs a text label for accessibility, make it a required prop with a TypeScript type. Don't let developers use the component without providing one.
- Provide sensible defaults. If your Alert component defaults to
role="alert"andaria-live="assertive", developers get accessibility for free. - Document the accessibility contract. Every component's documentation should include the keyboard interactions it supports, the ARIA roles and properties it uses, and any props the consumer needs to provide for accessibility.
- Include accessible examples. If your component documentation shows usage examples, those examples should demonstrate accessible usage... proper labels, correct ARIA attributes, keyboard handling.
Accessibility in a design system isn't about compliance; it's about building components that work for everyone by default. When accessibility is structural rather than decorative, it survives refactors, it scales with your team, and it stops being a recurring audit finding.
If you're building or refactoring a component design system and want accessibility built into the foundation, let's talk about your component architecture.