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 AriaHeadless behavior and semanticsARIA, keyboard, focus, announcements, i18n
Tailwind CSSUtility stylingNothing semantic; expresses our visual decisions
Untitled UI ReactStyled components on React AriaStructure and default styling
DrumKitTheme, tokens, compositionContrast, 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

Arrow keys, typeahead, and selection modifiers per the APG pattern.

Focus management

Contained in overlays, restored on close, moved sensibly on removal.

Keyboard-only focus rings

Shown for keyboard focus, never left behind by pointer clicks.

No sticky hover on touch

The hover state is never applied on touch devices.

Press semantics

One press state for click, tap, and Enter/Space alike.

Touch screen readers

Everything works without a keyboard, dialogs stay dismissable.

Internationalisation

30+ locales, RTL layout, calendar and numbering systems.

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-onlyVisually hidden text for screen readers26 uses
aria-hiddenHiding decorative elements from AT167 uses
focus-visible:Keyboard-only focus styling331 uses
outline-hiddenForced-colors-safe outline removal83 uses
motion-reduce: / motion-safe:Respecting prefers-reduced-motion21 motion-reduce: uses
contrast-more: / forced-colors:High-contrast and forced-colors modesforced-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 touchavailable, 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

Its claim describes React Aria’s guarantees, not an independent audit or VPAT.

Not everything is React Aria

Marketing sections and page examples are plain markup; review them by hand.

Charts are not covered

Recharts renders SVG with no semantics; every chart needs a text alternative.

We own the code

The kit is vendored; upstream fixes arrive only when we pull them in.

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

At least 4.5:1 against its background.

Large text

24px, or 18.66px bold and above: at least 3:1.

UI boundaries

Borders, icons, focus rings, chart series: at least 3:1.

Both themes

Every ratio holds in light and dark, and every brand variation.

Never color alone

Status and validation always carry text, icon, or shape.

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 16px icon inside a 40×40 target: padding grows the target, not the icon.

A bare small target

16×16 with no padding sits under the 24px floor: too easy to miss.

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

Every input has a visible label. Placeholder text is not a label.
aria-label="Delete invoice"

Icon-only still has a name

Icon-only buttons, close buttons, and clear buttons all carry an aria-label.

Names carry the object

Accessible names describe the action and its object, not just “Delete”.
Enter the 16-digit number on the front

Errors that help

Errors say what is wrong and how to fix it, tied to their field. Never color alone.
aria-label
aria-hidden

Meaningful vs decorative icons

Icons that carry meaning get names; decorative icons are hidden from assistive tech.
h1
h2
h3

Headings are structural

Levels describe the content beneath them and never skip for visual reasons.

Composition

Individually accessible components can compose into an inaccessible page. The rules that keep a page whole:

One h1 per page

Heading levels chosen for structure, never for size.

Landmarks present and unique

So screen reader users can jump between regions.

A skip link, reachable first

Straight past the chrome to the main content.

Focus follows the action

Moved to revealed content only when the user asked for it.

Dialogs return focus

Never leave focus on an element that no longer exists.

Live regions, sparingly

Used for what genuinely changes: not everything, not nothing.

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.