Accessibility
DrumKit is built to be accessible by default, but using DrumKit does not by itself make a product accessible: what DrumKit guarantees, what it inherits from the layers underneath it, and what you still own every time you build a screen.
Our conformance target
DrumKit targets WCAG 2.2 Level AA. Meeting that target in a shipped product depends on how components are composed, labeled, and filled with content. DrumKit gives you an accessible floor; it cannot give you an accessible product. What has and hasn’t been verified lives in Known gaps on this page.
How accessibility works in DrumKit
DrumKit is four layers. Each is responsible for different things, and knowing which layer owns what is the fastest way to debug an accessibility problem. The short version: behavior is inherited, appearance is ours. Almost every accessibility defect we are likely to ship is an appearance or composition defect, not a behavior defect.
Layer | What it is | Responsible for |
|---|---|---|
| React Aria | Headless behavior and semantics | ARIA, keyboard, focus, announcements, i18n |
| Tailwind CSS | Utility styling | Nothing semantic; expresses our visual decisions |
| Untitled UI React | Styled components on React Aria | Structure and default styling |
| DrumKit | Theme, tokens, composition | Contrast, focus, targets, motion, labeling |
What React Aria gives us
React Aria is Adobe’s headless component library, maintained as part of React Spectrum. It implements semantics and keyboard behavior per the W3C ARIA Authoring Practices Guide and WAI-ARIA. Everything below comes free on React Aria–backed components.
Keyboard interaction
Focus management
Keyboard-only focus rings
No sticky hover on touch
Press semantics
Touch screen readers
Internationalisation
React Aria tests against VoiceOver on macOS (Safari and Chrome), JAWS on Windows (Firefox and Chrome), NVDA on Windows (Firefox and Chrome), VoiceOver on iOS, and TalkBack on Android (Chrome).
What React Aria explicitly does not do. You are responsible for an accessible visual design: meaningful labels, sufficient color contrast, adequate hit target sizes, visible focus rings, and respecting motion preferences. Every one of those is DrumKit’s job, not React Aria’s.
Check the false-positives list before filing a bug. React Aria maintains a list of known false positives that automated accessibility tools report against its components. Cross-check an axe or Lighthouse run against it before filing a DrumKit bug.
Before filing a DrumKit bug from an axe or Lighthouse run, check React Aria’s known false positives wiki.
What Tailwind CSS does and does not do
Tailwind is presentational. It has no semantics, no roles, and no keyboard behavior. It cannot make a component accessible, and it cannot make one inaccessible, except through the visual decisions we express with it, which is precisely where most accessibility failures live.
Utility / variant | Use | In the kit today |
|---|---|---|
sr-only | Visually hidden text for screen readers | 26 uses |
aria-hidden | Hiding decorative elements from AT | 167 uses |
focus-visible: | Keyboard-only focus styling | 331 uses |
outline-hidden | Forced-colors-safe outline removal | 83 uses |
motion-reduce: / motion-safe: | Respecting prefers-reduced-motion | 21 motion-reduce: uses |
contrast-more: / forced-colors: | High-contrast and forced-colors modes | forced-colors: adopted 2026-08-22 across the base fields and selects, radios, button groups, badges, progress bars, the avatar status dot and featured icons, and on the application section’s breadcrumbs, color pickers, command menus, pagination and tabs (activity feeds and notifications got real borders instead); contrast-more: not yet |
pointer-coarse: / pointer-fine: | Larger targets on touch | available, not yet adopted |
The outline-none trap
In Tailwind v3, outline-none did not actually remove the outline; it applied a 2px transparent outline, which stays visible in forced colors mode. Tailwind v4 renamed that behavior to outline-hidden and introduced a new outline-none that genuinely sets outline-style: none. This matters because box shadows are not rendered in forced colors mode: a focus ring built from shadow-* or ring-* on top of outline-none vanishes for a Windows High Contrast user.
Never use outline-none
Use outline-hidden and pair it with an explicit visible focus style that is not itself an outline-* utility. In Tailwind v3 outline-none applied a 2px transparent outline that stays visible in forced colors mode; v4 renamed that to outline-hidden and made outline-none genuinely set outline-style: none. Box shadows are not rendered in forced colors mode, so a focus ring built from shadow-* or ring-* on top of outline-none vanishes for a Windows High Contrast user. One pairing does not compose in v4: outline-hidden sets --tw-outline-style to none, and focus-visible:outline-2 reads that variable back as its outline-style, so the ring never paints. Measured 2026-08-22 on CloseButton, which had no keyboard ring that way until its focus:outline-hidden was dropped. When the ring is an outline-* utility, leave outline-hidden off, since browsers draw no default ring on pointer focus, or add focus-visible:outline-solid. The vendored kit follows this today: 83 outline-hidden uses, zero outline-none.
What Untitled UI adds, and what it doesn’t
DrumKit vendors the full Untitled UI React kit (built on React, Tailwind CSS v4, TypeScript, and React Aria). Untitled UI describes itself as accessible on the basis that it is built on React Aria components following WAI-ARIA standards. That is accurate as far as it goes; be precise about what it means.
Accessibility by inheritance
Not everything is React Aria
Charts are not covered
We own the code
The upstream practice: watch the Untitled UI changelog and React Aria releases, pull accessibility fixes in per release, and record the versions DrumKit is aligned to on every sync.
What DrumKit owns
These are ours. Every one of them is a place where we can break accessibility despite everything above being correct.
Color and contrast
DrumKit re-themes Untitled UI rather than using its palette as shipped, which means none of Untitled UI’s contrast characteristics carry over to our tokens. Contrast is our responsibility to verify and re-verify whenever the palette changes.
Body text
Large text
UI boundaries
Both themes
Never color alone
Focus visibility
--color-focus-ring (brand-500) rendered as a 2px outline with a 2px offset via focus-visible:outline-2 focus-visible:outline-offset-2, with --color-focus-ring-error (rose-500) for invalid fields. Every interactive element shows it on keyboard focus, it must meet 3:1 contrast against the adjacent background, and focus order follows visual reading order — with the one narrow exception below. Both rings are rendered live on the Effect styles page. See the outline-none rule above for the implementation constraint.
Rings drawn statically: brand-500 and rose-500, 2px at a 2px offset; tab anywhere on this site for the real thing
Target size
- Interactive targets are at least 24×24 CSS pixels. WCAG 2.2 AA, success criterion 2.5.8.
- Grow the hit area, not the visual. Where a control is visually smaller, expand the hit area with padding rather than by scaling the visual.
- Space adjacent targets. Adjacent targets have enough spacing that the 24px exception never has to apply.
✓ Pad the hit area
✗ A bare small target
Motion
- Every animation gets a reduced-motion path. The standard and its current honest status live on the Motion page.
- Nothing flashes more than three times per second.
- Auto-playing motion longer than five seconds can be paused. Paused, stopped, or hidden.
- Parallax and large-scale movement always respect the preference. They are vestibular triggers, so they always respect prefers-reduced-motion.
Reduced motion removes the movement and keeps the fade: the state change stays legible
Labeling and content
React Aria associates a rendered <Label> with its control automatically. Our responsibility is that the label exists and says something useful; the full standard lives on the Content design page.
A visible label, always
aria-label="Delete invoice"Icon-only still has a name
Names carry the object
Errors that help
aria-labelaria-hiddenMeaningful vs decorative icons
Headings are structural
Composition
Individually accessible components can compose into an inaccessible page. The rules that keep a page whole:
One h1 per page
Landmarks present and unique
A skip link, reachable first
Focus follows the action
Dialogs return focus
Live regions, sparingly
How we test
The process as it actually runs today: systematised where the tooling exists, manual where it doesn’t yet.
- The accessibility review gate. UI changes run through the plugin’s /accessibility-review skill and its read-only review agent before merge: semantic HTML, labels, keyboard navigation, focus, ARIA usage, and contrast, producing a pass/fail report with required fixes.
- The template gates. Every change passes lint, typecheck, unit tests, a production build, and the Playwright e2e suite.
- Keyboard pass. Tab through the changed flow: every interactive element reachable, in a sensible order, with a visible focus ring, and no keyboard traps.
- Both themes. New and changed surfaces are checked in light and dark mode.
- Not yet systematic. Automated axe scans, screen reader passes, per-token contrast verification, forced-colors and reduced-motion emulation. These are listed honestly in Known gaps.
Known gaps
A short, honest gap list is worth more than a long list of principles. What has not been done, as of the last update of this page:
- No component has been systematically screen-reader tested by us; we inherit React Aria’s testing, and spot-checks are ad hoc.
- The DrumKit palette has not been contrast-verified pair-by-pair across both themes; verification is per-change judgement.
- The kit carries motion-reduce: variants only on its CSS loops (the loading-indicator spinners and the empty-state and social-proof marquees); its motion-library usages do not call useReducedMotion(), so the reduced-motion standard holds only there, for new work, and for the docs site.
- Target sizes have the same status as contrast: required, not yet audited pair-by-pair across the kit’s smallest controls.
- Presentational (non–React Aria) kit components, such as marketing sections and page examples, have not been individually reviewed.
- Chart components have no built-in text alternatives; each use needs its own.
- This documentation site has not itself been audited against the target.
Checklist
The per-screen distillation of everything above. Before UI ships:
- Every interactive element is keyboard-reachable, in a sensible order, with the visible focus ring, and no keyboard traps
- Text contrast 4.5:1; large text and UI boundaries 3:1, in both themes
- Interactive targets at least 24×24px, hit areas padded rather than visuals scaled
- Every input has a visible label; every icon-only button an aria-label
- Color is never the only signal: status and validation carry text, icon, or shape
- Headings are structural, landmarks present, focus moves with newly revealed content
- Every animation has a reduced-motion path
- Automated scan results cross-checked against React Aria’s false-positives list
- Anything skipped is recorded in Known gaps, not left unstated
Reporting an accessibility issue
Open a GitHub issue on the repository. Include the component, the assistive technology and browser used, what you expected, and what happened. Accessibility issues are prioritized above other bug classes.
References
The primary sources behind every claim on this page.

