Yuriy Balaka

Accessibility checks for design systems built on shadcn

What was wrong

shadcn is a common way to build a design system. A team copies the ready components and restyles them. Under shadcn sits Base UI, a component library with its own tests for keyboard, focus and screen readers. Nobody tests the look: contrast, the focus ring, a status shown by colour alone. The look is what breaks when a team, or an AI agent, restyles a component.

A clean shadcn install already fails:

  • the field border has 1.26:1 contrast against the background; WCAG asks for 3:1;
  • the text on the red “destructive” button has 3.98:1; WCAG asks for 4.5:1;
  • the focus ring has 1.54:1.

axe, the common checker, catches the red button only. Ask an AI agent “is the contrast fine?” and it says “fine” without measuring. The person who has to sign off has nothing to sign.

What I decided

  • A rule is written once, from the WCAG 2.2 text copied word for word, with pass and fail examples in numbers.
  • Every check has three outcomes: passed, failed, or “cannot tell”. “Cannot tell” goes to a person.
  • A person’s decision on a disputed case is recorded: what, which text of the standard, who, why, until when. An agent may draft a decision. Only a person on the approvers list can approve it.
  • The check that refuses an edit cannot be talked around from the chat. It also refuses changes to itself.

What I built

A repository with a clean shadcn project and Storybook. The rules run inside Storybook, on the same Accessibility tab as axe, and on every edit. Here is one rule from start to end:

  1. Storybook shows the field “border contrast 1.26:1, needs 3:1” next to axe’s own findings.
  2. An agent asked to fix it changes the border colour in both themes, runs the check and quotes the numbers.
  3. Asked to set a paler border “as a design decision”, it cannot: the edit is refused with “1.35:1, needs 3:1”. The agent’s own words: “a designer’s decision doesn’t change the measured ratio”.
  4. A person who wants that colour anyway records a decision and approves it. The same edit then goes through, marked.

To follow it yourself, open the live demo, go to WCAG compliance › Overview and follow the four steps.

1.26:1 ✕ fails, needs 3:1
Field border against the background, measured as you move it.
ruleStorybookevery editrefusedaccepted

What changed

  • The whole design system is covered: 58 components, 445 examples from the official shadcn docs.
  • This repository’s theme now passes border contrast: 3.02:1 light, 4.08:1 dark.
  • All 70 axe rules for levels A and AA run under their WCAG number.
  • Against the W3C’s own test cases: 24 rules agree, 5 agree in part, 8 disagree.
  • The same check runs on pull requests, so a failing component cannot be merged.

What is left

  • My own code for the 8 rules that disagree with the W3C.
  • One-command install into your own Storybook.