Skip to content

Getting Started

Real A11y is a suite of packages built around a single engine: @real-a11y-dev/core. Each package is independently installable — use just the ones you need.

Pick your entry point

I want to…InstallDep type
Embed an interactive tree panel in any web app@real-a11y-dev/inspectordev (recommended)
Write accessibility assertions in Vitest / Jest@real-a11y-dev/testingdev
Run accessibility assertions in Playwright E2E tests@real-a11y-dev/testing/playwrightdev
Use React hooks and a <SemanticNavigator /> component@real-a11y-dev/reactdev (recommended)
Add an A11y tree panel to every Storybook story@real-a11y-dev/storybook-addondev
Build your own tooling on the extraction engine@real-a11y-dev/coredepends (see below)

Install as a dev dependency by default

Real A11y is a developer-time audit suite, not runtime infrastructure. Install everything under devDependencies (-D / --save-dev) and gate any UI it renders on a development build flag. That way the tree extractor, Preact renderer, and accessibility heuristics never reach your production bundle.

See Keep it out of production below for the gating patterns.

The only time you want a runtime dependency is if you're building your own published tooling on top of @real-a11y-dev/core (e.g. another testing library or an audit service) — then core belongs in your regular dependencies or, better, peerDependencies.


Quick install

All packages are installed as dev dependencies below. This is the right default for apps.

sh
# Framework-agnostic embed (dev panel)
npm install -D @real-a11y-dev/inspector

# Testing helpers (Vitest / Jest)
npm install -D @real-a11y-dev/testing

# Playwright E2E adapter (needs @playwright/test as peer)
npm install -D @real-a11y-dev/testing @playwright/test

# React wrapper — hooks + <SemanticNavigator /> component
npm install -D @real-a11y-dev/react

# Storybook addon
npm install -D @real-a11y-dev/storybook-addon
sh
pnpm add -D @real-a11y-dev/inspector
pnpm add -D @real-a11y-dev/testing
pnpm add -D @real-a11y-dev/testing @playwright/test
pnpm add -D @real-a11y-dev/react
pnpm add -D @real-a11y-dev/storybook-addon
sh
yarn add -D @real-a11y-dev/inspector
yarn add -D @real-a11y-dev/testing
yarn add -D @real-a11y-dev/testing @playwright/test
yarn add -D @real-a11y-dev/react
yarn add -D @real-a11y-dev/storybook-addon

Your first tree in 30 seconds

Framework-agnostic embed

ts
import { createInspector } from "@real-a11y-dev/inspector";

const sn = createInspector({
  root: document.getElementById("app")!,
  container: document.getElementById("sn-panel")!,
  mode: "a11y",
  // Shadow DOM isolation is on by default — no CSS conflicts.
});

sn.mount();

The panel renders inside a ShadowRoot attached to #sn-panel. Your app's CSS cannot leak in; the panel's CSS cannot leak out.


React

tsx
import { SemanticNavigator, useSemanticTree } from "@real-a11y-dev/react";

function App() {
  const rootRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={rootRef}>
      <YourApp />
      <SemanticNavigator root={rootRef} mode="a11y" />
    </div>
  );
}

Or use the hook for headless access to tree data:

tsx
function TreeDebugger({ rootRef }) {
  const tree = useSemanticTree(rootRef, { mode: "a11y" });

  if (!tree) return null;
  return <pre>{JSON.stringify(tree, null, 2)}</pre>;
}

Testing (Vitest / Jest)

ts
import { auditSnapshot, assertNoUnlabeledInteractive } from "@real-a11y-dev/testing";
import { render } from "@testing-library/react";

test("login form is fully labeled", () => {
  const { container } = render(<LoginForm />);

  // Throws with a clear message if any interactive element is unlabeled.
  assertNoUnlabeledInteractive(container);

  // Deterministic string snapshot of the A11y tree — great for CI.
  expect(auditSnapshot(container)).toMatchSnapshot();
});

Playwright (E2E)

ts
import { test, expect } from "@playwright/test";
import { attach } from "@real-a11y-dev/testing/playwright";

test("home page accessibility", async ({ page }) => {
  await page.goto("/");
  const sn = await attach(page);

  // Structural assertions — throw with descriptive messages on failure
  await sn.assertHeadingOrder();
  await sn.assertNoUnlabeledInteractive();
  await sn.assertLandmarkStructure();

  // Deterministic snapshot — commit to version control
  expect(await sn.auditSnapshot()).toMatchSnapshot();
});

The adapter injects the audit engine into the real browser page. No changes to your app code required.


Storybook addon

Add to .storybook/main.ts:

ts
export default {
  addons: ["@real-a11y-dev/storybook-addon"],
};

That's it. A Semantic Navigator panel appears next to Controls and A11y for every story, showing the tree, heading outline, and tab sequence — updated live as the story re-renders.


Requirements

PackageNodeBrowser / Runtime
@real-a11y-dev/core≥ 20Any modern browser, jsdom
@real-a11y-dev/inspector≥ 20Modern browser (Shadow DOM required)
@real-a11y-dev/testing≥ 20jsdom or real browser
@real-a11y-dev/react≥ 20React ≥ 18, modern browser
@real-a11y-dev/storybook-addon≥ 20Storybook ≥ 8

Keep it out of production

@real-a11y-dev/inspector and @real-a11y-dev/react render a full tree view (~30 KB gzipped, plus Preact). If you drop <SemanticNavigator /> or createInspector() into a component that ships to production, bundlers can't tree-shake the renderer away — the reference exists at runtime. You want it dev-only.

Two patterns, pick the one that matches your toolchain.

Pattern 1 — static gate (ideal, fully tree-shaken)

Your bundler erases a false-branch at build time. Works with Vite, Rollup, webpack, esbuild.

tsx
// DevA11y.tsx
import { lazy, Suspense } from "react";

const SemanticNavigatorLazy = lazy(async () => {
  const mod = await import("@real-a11y-dev/react");
  return { default: mod.SemanticNavigator };
});

export function DevA11y({ rootRef, ...props }) {
  if (!import.meta.env.DEV) return null; // Vite replaces with `false` in prod
  return (
    <Suspense fallback={null}>
      <SemanticNavigatorLazy root={rootRef} {...props} />
    </Suspense>
  );
}
tsx
// DevA11y.tsx
import dynamic from "next/dynamic";

const SemanticNavigatorDyn = dynamic(
  () => import("@real-a11y-dev/react").then((m) => m.SemanticNavigator),
  { ssr: false }
);

export function DevA11y({ rootRef, ...props }) {
  if (process.env.NODE_ENV !== "development") return null;
  return <SemanticNavigatorDyn root={rootRef} {...props} />;
}
ts
// dev-inspector.ts
export async function mountDevInspector(root: HTMLElement, container: HTMLElement) {
  if (process.env.NODE_ENV === "production") return; // erased in prod builds
  const { createInspector } = await import("@real-a11y-dev/inspector");
  createInspector({ root, container, mode: "a11y" }).mount();
}

Use it anywhere — it's a no-op in production, and the import(...) is code-split so the inspector chunk only loads when the gate is true.

Pattern 2 — runtime query param (useful for staging)

You want to audit a deployed staging URL without a rebuild. Enable via ?a11y=1.

tsx
import { lazy, Suspense, useSyncExternalStore } from "react";

const DevInspector = lazy(() =>
  import("@real-a11y-dev/react").then((m) => ({ default: m.SemanticNavigator }))
);

function useA11yFlag() {
  return useSyncExternalStore(
    (cb) => { window.addEventListener("popstate", cb); return () => window.removeEventListener("popstate", cb); },
    () => new URLSearchParams(location.search).has("a11y"),
    () => false,
  );
}

export function OptionalA11y(props) {
  const enabled = useA11yFlag();
  if (!enabled) return null;
  return <Suspense fallback={null}><DevInspector {...props} /></Suspense>;
}

This ships the inspector into the production bundle (as a separate chunk) — acceptable for internal apps / staging, not ideal for consumer-facing production. Prefer Pattern 1 when you can.

What's safe to leave in production

PackageSafe in prod?Notes
@real-a11y-dev/core✅ small, no UIOnly if you use it at runtime (most apps don't). ~8 KB gzipped.
@real-a11y-dev/inspector⚠ gate itShips Preact + tree view. ~40 KB gzipped total.
@real-a11y-dev/react⚠ gate itTransitively includes @real-a11y-dev/inspector.
@real-a11y-dev/testing❌ dev-onlyTest helpers — -D.
@real-a11y-dev/storybook-addon❌ dev-onlyStorybook-only — -D.

Next steps

  • Core Concepts — understand the semantic tree model, roles, and names
  • Why Real A11y? — how this compares to axe-core, Testing Library, and other tools

Released under the MIT License. · Privacy · Accessibility