Skip to main content

Accessibility Testing in Storybook: WCAG Compliance

Accessibility testing in Storybook is powered by the @storybook/addon-a11y add-on, which integrates Deque's axe-core library into every story. With one click, you scan a rendered component for WCAG 2.1 level AA violations—missing ARIA labels, low contrast text, improper heading hierarchies, and semantic markup issues. Fixing accessibility issues before merging code into your main branch prevents accessibility regressions and ensures your React components are usable by all users, including those using assistive technologies. The best part: axe scans are fast (under 1 second per story) and provide clear remediation guidance.

Installing and Enabling the Accessibility Add-on

After Storybook init (covered in the first article), the scaffolder usually includes @storybook/addon-a11y by default. If not, install it:

npm install --save-dev @storybook/addon-a11y

Then add it to .storybook/main.ts:

import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y', // Add this line
],
framework: '@storybook/react-webpack5',
};

export default config;

Restart your Storybook dev server. You'll now see an "Accessibility" tab in the right panel next to "Controls" and "Actions." Click any story and the tab will auto-run an axe audit.

Understanding Accessibility Violations

When you open the Accessibility tab, axe scans the rendered story and categorizes findings:

CategoryDescriptionAction
ViolationsIssues that fail WCAG 2.1 AA; must fixCritical: fix before merging
PassesComponents passing accessibility checksGood—no action needed
WarningsBest-practice issues; context-dependentReview; may require domain expertise
IncompleteIssues needing manual review (color contrast in images)Review; not always fixable automatically

A simple Button with missing text alt text triggers a "Missing alt text" violation (rule image-alt). The audit displays:

  • Issue: Images must have alternate text
  • Element: <img src="icon.png" />
  • Fix: Add alt="button icon" to the image.
  • WCAG Reference: WCAG 1.1.1 Non-text Content

Writing Accessible Components with Semantic HTML

The best way to pass accessibility audits is to write semantically correct HTML from the start. Here's an example of a poorly accessible form, followed by an improved version:

Bad (violations: missing labels, no fieldset):

export const BadForm = () => (
<form>
<input placeholder="Email" />
<input placeholder="Password" type="password" />
<button>Sign In</button>
</form>
);

The audit flags this with:

  • form-has-visible-label: Inputs lack associated labels.
  • input-image-alt: The form lacks ARIA descriptions.

Good (semantic HTML, proper labels):

export const AccessibleForm = () => (
<form>
<fieldset>
<legend>Sign In</legend>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" required />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" type="password" required />
</div>
<button type="submit">Sign In</button>
</fieldset>
</form>
);

This version passes the audit because:

  • Each input has an explicit <label> with matching htmlFor and id.
  • The form is wrapped in <fieldset> with a <legend>, providing context.
  • Button type is explicit (type="submit").

Configuring axe Rules and Disabling Checks

Some axe rules may not apply to your component. You can disable specific checks in a story's parameters:

import type { Meta, StoryObj } from '@storybook/react';
import { Modal } from './Modal';

const meta = {
component: Modal,
tags: ['autodocs'],
} satisfies Meta<typeof Modal>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
isOpen: true,
title: 'Delete Confirmation',
},
parameters: {
a11y: {
config: {
rules: [
{
id: 'color-contrast', // Disable color contrast check for this story
enabled: false,
},
],
},
options: {
wcagVersion: '2.1', // Target WCAG 2.1 (default)
},
},
},
};

Use this sparingly—disabling checks hides problems rather than fixing them. Only disable when the check is inapplicable (e.g., a modal with a semi-transparent overlay may trip color contrast warnings despite proper styling).

Testing with Screen Readers in Mind

Write component stories that are screen-reader friendly. Use ARIA attributes, semantic HTML, and descriptive link text:

interface ButtonProps {
label: string;
icon?: React.ReactNode;
ariaLabel?: string;
}

export const Button: React.FC<ButtonProps> = ({ label, icon, ariaLabel }) => (
<button aria-label={ariaLabel || label}>
{icon && <span aria-hidden="true">{icon}</span>}
{label}
</button>
);

// Story with proper a11y
export const WithIcon: Story = {
args: {
label: 'Download',
icon: '⬇',
ariaLabel: 'Download the current document',
},
};

Key practices:

  • aria-label: Provides a label for screen readers when visual text is insufficient.
  • aria-hidden="true": Hides decorative icons from screen readers so they don't clutter the announcement.
  • Semantic tags: Use <button>, <nav>, <main>, <article> instead of <div> with ARIA roles.

Testing Keyboard Navigation with Accessibility Audits

The accessibility audit includes a "Keyboard" check that verifies all interactive elements are keyboard-accessible. Pair this with play functions to test keyboard workflows:

export const KeyboardAccessible: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Tab through all buttons
const buttons = canvas.getAllByRole('button');
for (const button of buttons) {
await userEvent.tab();
await expect(button).toHaveFocus();
}
},
parameters: {
a11y: {
config: {
rules: [
{ id: 'page-has-heading-one', enabled: false }, // Not all components are pages
],
},
},
},
};

Combining play functions with accessibility audits tests both automation and manual keyboard navigation in one story.

Creating an Accessibility Baseline Report

Run Storybook with the --automation flag and export accessibility results to a JSON file for CI/CD integration:

npm run storybook -- --automation --output-dir storybook-results

This generates a report that you can commit to version control and track accessibility regressions over time.

Key Takeaways

  • @storybook/addon-a11y integrates axe-core: Every story auto-scans for WCAG 2.1 AA violations with one click in the Accessibility tab.
  • Fix violations at the source: Write semantic HTML (<label>, <fieldset>, <button>) and use proper ARIA attributes (aria-label, aria-hidden).
  • Violations vs. warnings: Violations are critical (WCAG compliance); warnings are best practices. Fix violations before shipping.
  • Disable checks only when inapplicable: Use parameters.a11y.config.rules to disable a check, but only for genuinely inapplicable cases.
  • Screen readers matter: Use aria-label for icon-only buttons, aria-hidden for decorative elements, and descriptive link text.
  • Combine with keyboard testing: Pair play functions with accessibility audits to test keyboard navigation alongside automated scans.

Frequently Asked Questions

What's the difference between WCAG 2.1 Level A and AA?

WCAG 2.1 Level A is a baseline standard with essential accessibility requirements. Level AA (the industry standard) includes stricter rules like minimum color contrast ratios (4.5:1 for normal text). Level AAA is the highest standard, rarely required unless mandated by law. Storybook defaults to AA, which is appropriate for most projects.

Can I run accessibility audits in CI/CD pipelines?

Yes. Use the @storybook/test-runner package to run stories (including accessibility audits and play functions) headlessly in CI: npm run test-storybook -- --coverage. This generates a report you can fail the build if violations are found.

How do I test color contrast automatically?

Axe-core's color-contrast rule auto-checks text-to-background ratios and compares against WCAG thresholds. If a test fails, the audit displays the actual and required contrast ratios. Manual review is sometimes needed (e.g., for image-based text or anti-aliased graphics).

What's the right way to hide elements from screen readers?

Use aria-hidden="true" for decorative elements (icons, borders). Use display: none or visibility: hidden for elements that should be invisible to all users. Never use display: none to hide content from sighted users while keeping it in the accessibility tree—that confuses screen reader users.

Further Reading