--- url: /a11y-hud/guide.md description: >- Get started with a11y-hud — install the package, mount the overlay, and run your first accessibility audit in any framework. --- # Getting Started a11y-hud is a dev-only overlay that runs [axe-core](https://github.com/dequelabs/axe-core) accessibility audits inside your running web app. It renders a floating panel listing every detected violation — no browser extension, no build-time transformation, no framework lock-in. ![a11y-hud panel open showing violations, with filter chips expanded](/img/filter-ux.gif) ## Who it's for * **App developers** doing local a11y work during development. * **QA / a11y reviewers** poking at staging without installing axe DevTools. ## Installation ::: code-group ```bash [npm] npm install --save-dev a11y-hud ``` ```bash [pnpm] pnpm add -D a11y-hud ``` ```bash [yarn] yarn add -D a11y-hud ``` ::: **CDN / script tag** — no install needed: ```html ``` ## First mount ### Imperative (any framework) ```js import { mount } from "a11y-hud"; // Gate behind DEV check so it never ships to production if (import.meta.env.DEV) { mount({ theme: "auto" }); } ``` ### Declarative (script-tag path) ```html ``` ### Click to highlight Clicking a node selector in the panel outlines the corresponding element on the page. ![Clicking a violation node highlights the element on the page with a purple outline](/img/click-to-highlight.gif) ### Headless (no UI) `runScan()` audits the page without rendering the panel. Run it in the browser — for example from the DevTools console of an app that already imports `a11y-hud`, or inside a Playwright `page.evaluate()`: ```js import { runScan } from "a11y-hud"; const results = await runScan(document.body); if (results.violations.length > 0) { console.error("Accessibility violations found:", results.violations); } ``` For failing CI builds on violations, see the [CI integration cookbook](/cookbook/ci-integration). ## Framework adapters If you're using a framework, pick the matching adapter for a tighter integration that rescans automatically when your component tree re-renders. | Framework | Package | |-----------|---------| | React | [`@a11y-hud/react`](/guide/react) | | Vue 3 | [`@a11y-hud/vue`](/guide/vue) | | Angular | [`@a11y-hud/angular`](/guide/angular) | | Svelte 5 | [`@a11y-hud/svelte`](/guide/svelte) | | Solid | [`@a11y-hud/solid`](/guide/solid) | | Astro | [Guide (no separate package)](/guide/astro) | | Qwik | [Guide (no separate package)](/guide/qwik) | ## What not to use it for * **Production sites** — mount only in development. The HUD adds axe-core (~300KB) to the page. * **Storybook** — `@storybook/addon-a11y` is purpose-built for that workflow. * **Server-side scanning** — axe-core requires a live browser DOM. Importing `a11y-hud` on the server is safe (the Custom Element is registered only when `customElements` exists), but the HUD mounts and scans only in the browser. --- --- url: /a11y-hud/guide/vanilla.md --- # Vanilla / Script tag The vanilla core (`a11y-hud`) is a complete product on its own — no framework required. ## Install ::: code-group ```bash [npm] npm install --save-dev a11y-hud ``` ```html [CDN] ``` ::: ## Quick start ### With a bundler (ESM) ```js import { mount } from "a11y-hud"; if (import.meta.env.DEV) { mount({ theme: "auto" }); } ``` ### Script tag (declarative) ```html ``` ### Script tag (imperative) ```html ``` ## Minimize to FAB Pressing **−** or **Escape** collapses the panel to a small pill. Clicking the pill reopens it. ![Minimizing the panel to a floating action button and reopening it](/img/minimize-fab.gif) ## Mount options ```js const hud = mount({ theme: "auto", // visual theme (default: "auto") scope: "#main-content", // CSS selector or Element to restrict scans autoScan: true, // re-scan on DOM mutations (default: true) debounce: 500, // debounce delay in ms (default: 500) runOnly: ["wcag2a", "wcag2aa"], // restrict to specific axe rule tags }); ``` `mount()` returns an [`A11yHudInstance`](/reference/api#a11yhudinstance) for runtime control. ## Scoping scans Pass any CSS selector or `Element` reference to restrict axe to a subtree: ```js mount({ scope: "#app" }); // or mount({ scope: document.getElementById("app") }); ``` ## Runtime control ```js const hud = mount({ theme: "github-dark" }); // Switch themes hud.setTheme("tokyo-night"); // Change which axe rule sets run hud.setRunOnly(["wcag2a"]); // Trigger a manual scan const results = await hud.runScan(); // Export results as JSON string const json = hud.exportResults(); // Remove the HUD from the page hud.unmount(); ``` ## Ignore rules The ignore list is stored in `localStorage` and survives page reloads. ```js // Ignore all violations for a rule hud.ignores.add("color-contrast"); // Ignore a specific node hud.ignores.add("label", "#my-input"); // List active ignores console.log(hud.ignores.list()); // Export / import as JSON (for sharing across team members) const json = hud.ignores.exportJson(); hud.ignores.importJson(json); // Clear all ignores hud.ignores.clear(); ``` ## Headless scan (no UI) ```js import { runScan } from "a11y-hud"; const results = await runScan(document.querySelector("#app")); console.log(results.violations); ``` `runScan()` needs a real browser DOM — it is not supported under Node/jsdom. See the [CI integration cookbook](/cookbook/ci-integration) for running it in Playwright. ## Bundle formats and SSR `a11y-hud` ships as ESM (`dist/index.js`) plus a UMD bundle (`dist/index.umd.js`, global `window.A11yHud`) for the script-tag and bookmarklet paths. There is no CommonJS build. Importing the package is safe in non-browser environments: the Custom Element is only registered when `customElements` exists, so server-side code can import `a11y-hud` as long as `mount()` is only called in the browser. ## Bookmarklet For QA reviewers on staging without build access, use the bookmarklet: ```js import { generateBookmarklet } from "a11y-hud"; // Always latest CDN version const url = generateBookmarklet(); // Pin to a specific version const pinned = generateBookmarklet("1.0.0"); ``` A pre-built drag-to-bookmark page is included in the npm package at `dist/bookmarklet.html`. See the [Bookmarklet cookbook](/cookbook/bookmarklet) for the full workflow. ## `` element attributes | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `theme` | `Theme` | `"auto"` | Visual theme | | `scope` | `string` | — | CSS selector for scan scope. Invalid selectors fall back to `document.body`; runtime changes re-target the observer | | `auto-scan` | presence | enabled | Remove attribute to disable auto-scan | | `debounce` | `number` | `500` | Auto-scan debounce delay in ms | | `run-only` | JSON string | — | Array of axe rule tags, e.g. `'["wcag2a"]'` | --- --- url: /a11y-hud/guide/react.md --- # React `@a11y-hud/react` is a thin React adapter that mounts the a11y-hud Custom Element and re-runs axe after every React render commit. ## Requirements * React 18 or later * `a11y-hud` (installed automatically as a dependency) ## Install ::: code-group ```bash [npm] npm install --save-dev @a11y-hud/react ``` ```bash [pnpm] pnpm add -D @a11y-hud/react ``` ```bash [yarn] yarn add -D @a11y-hud/react ``` ::: ## Quick start — component Add `` to your root component so it mounts once for the entire app: ```tsx [App.tsx] import { A11yHud } from "@a11y-hud/react"; export default function App() { return ( <> {import.meta.env.DEV && } {/* rest of app */} ); } ``` The component mounts the `` Custom Element to `document.body` and triggers a rescan after every React commit via `useEffect`. ## Quick start — hook Use `useA11yHud` when you need imperative access to the instance: ```tsx import { useEffect } from "react"; import { useA11yHud } from "@a11y-hud/react"; function DevTools() { const hud = useA11yHud({ theme: "auto" }); useEffect(() => { // Export results on demand document.addEventListener("keydown", (e) => { if (e.key === "e" && e.ctrlKey) { console.log(hud.exportResults()); } }); }, [hud]); return null; } ``` ## Server-side rendering Importing `@a11y-hud/react` is safe on the server (Next.js, Remix, React Router framework mode): the Custom Element is only registered when `customElements` exists. The HUD mounts inside `useEffect`, which never runs during SSR, so nothing is rendered server-side and no hydration mismatch occurs. In the Next.js App Router, place `` in a `"use client"` component. ## Props / options Both `` props and `useA11yHud` options accept the same fields: | Prop | Type | Default | Description | |------|------|---------|-------------| | `theme` | `Theme` | `"auto"` | Visual theme | | `scope` | `RefObject` | — | Ref to restrict scan scope | | `autoScan` | `boolean` | `true` | Re-scan on DOM mutations | | `debounce` | `number` | `500` | Auto-scan debounce in ms | | `runOnly` | `string[]` | — | axe rule tags to run | ## Scoping to a subtree ```tsx import { useRef } from "react"; import { A11yHud } from "@a11y-hud/react"; export default function App() { const scopeRef = useRef(null); return ( <>
{/* only this subtree is scanned */}
); } ``` ## Route-change rescans The adapter rescans after every React commit, which covers most route-change cases automatically when using React Router or TanStack Router. If you need explicit control, see the [Route-change rescans cookbook](/cookbook/route-change-rescans). ## Instance API `useA11yHud` returns the same [`UseA11yHudReturn`](/reference/api#useahudreturn) object across all adapters: ```ts const hud = useA11yHud({ theme: "auto" }); hud.runScan() // Promise — null if not mounted yet hud.setTheme("github-dark") hud.setRunOnly(["wcag2a"]) hud.exportResults() // JSON string | null hud.ignores.add("color-contrast") hud.ignores.list() // IgnoreEntry[] hud.ignores.exportJson() hud.ignores.importJson(json) hud.ignores.clear() ``` --- --- url: /a11y-hud/guide/vue.md --- # Vue 3 `@a11y-hud/vue` is a thin Vue 3 adapter that mounts the a11y-hud Custom Element in `onMounted` and re-runs axe when the `scope` prop changes (post-flush `watch`). General DOM updates are picked up by the core's MutationObserver. ## Requirements * Vue 3.4 or later * `a11y-hud` (installed automatically as a dependency) ## Install ::: code-group ```bash [npm] npm install --save-dev @a11y-hud/vue ``` ```bash [pnpm] pnpm add -D @a11y-hud/vue ``` ```bash [yarn] yarn add -D @a11y-hud/vue ``` ::: ## Quick start — component ```vue [App.vue] ``` ## Server-side rendering Importing `@a11y-hud/vue` is safe on the server (Nuxt, Vite SSR): the Custom Element is only registered when `customElements` exists. The HUD mounts in `onMounted`, which runs only in the browser, so nothing is rendered server-side. In Nuxt you can drop `` straight into `app.vue`; wrapping it in `` is optional. ## Quick start — composable ```ts import { useA11yHud } from "@a11y-hud/vue"; const hud = useA11yHud({ theme: "auto" }); ``` ## Props / options | Prop | Type | Default | Description | |------|------|---------|-------------| | `theme` | `Theme` | `"auto"` | Visual theme | | `scope` | `Element \| null` | — | Scan scope element. Pass a template ref directly (`:scope="myRef"`) — Vue templates auto-unwrap refs. | | `autoScan` | `boolean` | `true` | Re-scan on DOM mutations | | `debounce` | `number` | `500` | Auto-scan debounce in ms | | `runOnly` | `string[]` | — | axe rule tags to run | ## Scoping to a subtree Pass a Vue template ref as `scope`: ```vue ``` ## Route-change rescans Vue Router navigations mutate the DOM, which the core's MutationObserver picks up automatically (debounced). Changing the `scope` prop also triggers an immediate rescan. For explicit control, see the [Route-change rescans cookbook](/cookbook/route-change-rescans). ## Instance API ```ts const hud = useA11yHud({ theme: "auto" }); hud.runScan() // Promise — null if not mounted yet hud.setTheme("github-dark") hud.setRunOnly(["wcag2a"]) hud.exportResults() hud.ignores.add("color-contrast") hud.ignores.list() hud.ignores.exportJson() hud.ignores.importJson(json) hud.ignores.clear() ``` See [`UseA11yHudReturn`](/reference/api#useahudreturn) for full type details. --- --- url: /a11y-hud/guide/angular.md --- # Angular `@a11y-hud/angular` provides a standalone Angular component and injectable service for the a11y-hud Custom Element. ## Requirements * Angular 20–22 (peer range `>=20.0.0 <23.0.0`) * `a11y-hud` (installed automatically as a dependency) ## Install ::: code-group ```bash [npm] npm install --save-dev @a11y-hud/angular ``` ```bash [pnpm] pnpm add -D @a11y-hud/angular ``` ```bash [yarn] yarn add -D @a11y-hud/angular ``` ::: ## Quick start — component `A11yHudComponent` is a standalone component with selector `a11y-hud-angular`. ```typescript [app.component.ts] import { Component, isDevMode } from "@angular/core"; import { A11yHudComponent } from "@a11y-hud/angular"; @Component({ standalone: true, imports: [A11yHudComponent], template: ` @if (isDev) { } `, }) export class AppComponent { readonly isDev = isDevMode(); } ``` ## Quick start — service `A11yHudService` gives you imperative control without the component. It is declared with `@Injectable()` and no `providedIn`, so you must list it in the component's `providers` array — injecting it without a provider throws `NullInjectorError`. Call `init()` once to mount the HUD; the service unmounts it in `ngOnDestroy`. ```typescript import { Component, OnInit, inject } from "@angular/core"; import { A11yHudService } from "@a11y-hud/angular"; @Component({ standalone: true, providers: [A11yHudService], template: "", }) export class DevComponent implements OnInit { private hud = inject(A11yHudService); ngOnInit() { this.hud.init({ theme: "auto" }); } } ``` ## Server-side rendering Importing `@a11y-hud/angular` is safe on the server (Angular SSR, Analog): the Custom Element is only registered when `customElements` exists. The HUD mounts in `ngAfterViewInit` (component) or when you call `init()`, which run only in the browser, so nothing is rendered server-side. ## Inputs | Input | Type | Default | Description | |-------|------|---------|-------------| | `theme` | `Theme` | `"auto"` | Visual theme | | `scope` | `ElementRef \| Element \| null` | — | Element or ref for scan scope | | `autoScan` | `boolean` | `true` | Re-scan on DOM mutations | | `debounce` | `number` | `500` | Auto-scan debounce in ms | | `runOnly` | `string[]` | — | axe rule tags to run | `scope` accepts Angular's `@ViewChild` result directly: ```typescript @ViewChild("mainContent") mainContent!: ElementRef; // Pass to component via binding template: `` ``` ## Route-change rescans The component rescans after every render commit via `afterEveryRender`, which covers router navigations and re-renders that do not change any `@Input()` values. Input changes (`scope`, `theme`, `autoScan`, `debounce`, `runOnly`) are synced in `ngOnChanges`. For explicit router-driven rescans, see the [Route-change rescans cookbook](/cookbook/route-change-rescans). ## Instance API Both `A11yHudComponent` (via `@ViewChild`) and `A11yHudService` expose the same methods as [`UseA11yHudReturn`](/reference/api#useahudreturn): ```typescript @ViewChild(A11yHudComponent) hud!: A11yHudComponent; await this.hud.runScan(); // Promise — null if not mounted yet this.hud.setTheme("tokyo-night"); this.hud.setRunOnly(["wcag2aa"]); this.hud.exportResults(); // string | null this.hud.ignores.add("color-contrast"); ``` The service additionally exposes `init(options?)`, an `initialized` getter, and `syncScope` / `syncTheme` / `syncAutoScan` / `syncDebounce` for pushing option changes to the mounted element. See the [API reference](/reference/api#a11yhudservice) for details. --- --- url: /a11y-hud/guide/svelte.md --- # Svelte 5 `@a11y-hud/svelte` is a Svelte 5 adapter using runes that mounts the a11y-hud Custom Element and re-runs axe after DOM updates. ## Requirements * Svelte 5 (runes required) * `a11y-hud` (installed automatically as a dependency) ## Install ::: code-group ```bash [npm] npm install --save-dev @a11y-hud/svelte ``` ```bash [pnpm] pnpm add -D @a11y-hud/svelte ``` ```bash [yarn] yarn add -D @a11y-hud/svelte ``` ::: ## Quick start — component ```svelte [App.svelte] {#if isDev} {/if} ``` ## Quick start — hook `useA11yHud` accepts a getter function (not a plain object) so Svelte's `$effect` can track reactive `$props()` values: ```svelte ``` ## Server-side rendering Importing `@a11y-hud/svelte` is safe on the server (SvelteKit): the Custom Element is only registered when `customElements` exists. The HUD mounts in `onMount`, which runs only in the browser, so nothing is rendered server-side. You can place `` in `+layout.svelte` without any `browser` guard. ## Props / options | Prop | Type | Default | Description | |------|------|---------|-------------| | `theme` | `Theme` | `"auto"` | Visual theme | | `scope` | `Element \| null` | — | Element for scan scope (use `bind:this`) | | `autoScan` | `boolean` | `true` | Re-scan on DOM mutations | | `debounce` | `number` | `500` | Auto-scan debounce in ms | | `runOnly` | `string[]` | — | axe rule tags to run | ## Scoping to a subtree Use `bind:this` to get an element reference: ```svelte
``` ## Why a getter function for `useA11yHud` Svelte 5's `$effect` tracks reactive reads that happen during execution. If you spread `$props()` into a plain object before passing it, the effect cannot track the individual reactive property reads. Passing a getter closure preserves reactivity: ```ts // ✅ reactive — $effect tracks reads through the closure useA11yHud(() => ({ theme: props.theme })); // ❌ not reactive — spread loses prop reactivity const opts = { theme: props.theme }; useA11yHud(() => opts); ``` ## Route-change rescans `$effect` fires after Svelte DOM updates, covering router-driven re-renders automatically. For explicit control, see the [Route-change rescans cookbook](/cookbook/route-change-rescans). ## Instance API ```ts const hud = useA11yHud(() => ({ theme: "auto" })); hud.runScan() // Promise — null if not mounted yet hud.setTheme("solarized-dark") hud.setRunOnly(["wcag2a"]) hud.exportResults() hud.ignores.add("color-contrast") hud.ignores.list() ``` See [`UseA11yHudReturn`](/reference/api#useahudreturn) for full type details. --- --- url: /a11y-hud/guide/solid.md --- # Solid `@a11y-hud/solid` is a Solid adapter that mounts the a11y-hud Custom Element and re-runs axe via `createEffect` after reactive updates. ## Requirements * solid-js 1.8 or later * `a11y-hud` (installed automatically as a dependency) ## Install ::: code-group ```bash [npm] npm install --save-dev @a11y-hud/solid ``` ```bash [pnpm] pnpm add -D @a11y-hud/solid ``` ```bash [yarn] yarn add -D @a11y-hud/solid ``` ::: ## Quick start — component ```tsx [App.tsx] import { A11yHud } from "@a11y-hud/solid"; export default function App() { return ( <> {import.meta.env.DEV && } {/* rest of app */} ); } ``` ## Quick start — primitive `createA11yHud` follows Solid's `create*` naming convention for reactive primitives: ```tsx import { createA11yHud } from "@a11y-hud/solid"; function DevTools() { const hud = createA11yHud({ theme: "auto" }); return null; } ``` For reactive options (e.g., a theme signal), pass getter properties: ```tsx import { createSignal } from "solid-js"; import { createA11yHud } from "@a11y-hud/solid"; const [theme, setTheme] = createSignal("auto"); const hud = createA11yHud({ get theme() { return theme(); }, }); ``` ## Server-side rendering Importing `@a11y-hud/solid` is safe on the server (SolidStart): the Custom Element is only registered when `customElements` exists. The HUD mounts in `onMount`, which runs only in the browser, so nothing is rendered server-side. ## Props / options | Prop | Type | Default | Description | |------|------|---------|-------------| | `theme` | `Theme` | `"auto"` | Visual theme | | `scope` | `Element \| null` | — | Element for scan scope | | `autoScan` | `boolean` | `true` | Re-scan on DOM mutations | | `debounce` | `number` | `500` | Auto-scan debounce in ms | | `runOnly` | `string[]` | — | axe rule tags to run | ## Scoping to a subtree The `scope` prop is read reactively, so pass it from a signal — a plain `let` ref is `undefined` at render time and never updates: ```tsx import { createSignal } from "solid-js"; import { A11yHud } from "@a11y-hud/solid"; export default function App() { const [appRoot, setAppRoot] = createSignal(null); return ( <>
{/* only this subtree is scanned */}
); } ``` ## Route-change rescans `createEffect` tracks Solid's reactive graph, so rescans happen automatically after reactive updates including router navigations. For explicit control, see the [Route-change rescans cookbook](/cookbook/route-change-rescans). ## Instance API ```ts const hud = createA11yHud({ theme: "auto" }); hud.runScan() // Promise — null if not mounted yet hud.setTheme("github-dark") hud.setRunOnly(["wcag2aa"]) hud.exportResults() hud.ignores.add("label") hud.ignores.list() ``` See [`CreateA11yHudReturn`](/reference/api#createa11yhudreturn) for full type details. --- --- url: /a11y-hud/guide/astro.md --- # Astro Astro doesn't have a dedicated `@a11y-hud/astro` package because Astro delegates its UI to component islands (React, Vue, Svelte, Solid). a11y-hud works in Astro via two complementary paths. ## Path 1 — Vanilla core on the global page For Astro sites without islands (or to audit the full page including static content), use the vanilla core directly in a layout component: ```astro [layouts/BaseLayout.astro] --- const isDev = import.meta.env.DEV; --- {isDev && ( )} ``` `mount()` creates the `` element itself, so there is no need to also write the tag into the markup. Or with the CDN script tag: ```astro --- const isDev = import.meta.env.DEV; --- {isDev && ( <> )} ``` The vanilla core mounts once and auto-rescans on DOM mutations — it picks up island hydration automatically. ## Server-side rendering Astro renders every page on the server (or at build time). Importing `a11y-hud` or any adapter in that context is safe: the Custom Element is only registered when `customElements` exists. The HUD mounts only when the inline ` ``` ## Solid Router ```tsx import { createEffect } from "solid-js"; import { useLocation } from "@solidjs/router"; import { createA11yHud } from "@a11y-hud/solid"; function A11yHudWithRouter() { const hud = createA11yHud({ theme: "auto" }); const location = useLocation(); createEffect(() => { // location.pathname is a reactive signal location.pathname; hud.runScan(); }); return null; } ``` ## Vanilla (any router) ```js import { mount } from "a11y-hud"; const hud = mount({ theme: "auto" }); // Listen to the History API const originalPushState = history.pushState.bind(history); history.pushState = (...args) => { originalPushState(...args); hud.runScan(); }; window.addEventListener("popstate", () => hud.runScan()); ``` --- --- url: /a11y-hud/cookbook/ci-integration.md --- # CI integration Use the headless `runScan()` API to fail CI when accessibility violations are detected — no HUD UI, no browser extension required. ## Playwright (recommended) Playwright controls a real browser, giving axe-core a full, live DOM to audit. ### Setup Install the dependency alongside Playwright: ```bash npm install --save-dev a11y-hud @playwright/test ``` ### Basic test ```ts [tests/a11y.spec.ts] import { test, expect } from "@playwright/test"; test("homepage has no critical accessibility violations", async ({ page }) => { await page.goto("http://localhost:5173"); // a11y-hud is not pre-installed in the target app — inject the UMD bundle. // It is a classic script (not an ES module), so use addScriptTag, not import(). await page.addScriptTag({ url: "https://cdn.jsdelivr.net/npm/a11y-hud/dist/index.umd.js" }); const violations = await page.evaluate(async () => { const results = await window.A11yHud.runScan(document.body); return results.violations; }); const critical = violations.filter((v) => v.impact === "critical"); expect(critical, `Critical violations: ${critical.map((v) => v.id).join(", ")}`).toHaveLength(0); }); ``` ### Using the npm package in a test app If a11y-hud is already installed as a dev dependency in your test target app, import it directly: ```ts [tests/a11y.spec.ts] import { test, expect } from "@playwright/test"; test("no critical violations", async ({ page }) => { await page.goto("http://localhost:5173"); const violations = await page.evaluate(async () => { const { runScan } = await import("/node_modules/a11y-hud/dist/index.js"); const results = await runScan(document.body); return results.violations; }); expect(violations.filter((v) => v.impact === "critical")).toHaveLength(0); }); ``` ### Full violation report on failure ```ts test("no accessibility violations", async ({ page }) => { await page.goto("http://localhost:5173"); const violations = await page.evaluate(async () => { const { runScan } = await import("/node_modules/a11y-hud/dist/index.js"); const { violations } = await runScan(document.body); return violations.map((v) => ({ id: v.id, impact: v.impact, description: v.description, nodes: v.nodes.map((n) => n.target), })); }); if (violations.length > 0) { console.table(violations); } expect(violations).toHaveLength(0); }); ``` ## Why not Node.js + jsdom? Importing `a11y-hud` in Node is safe (nothing runs until `mount()` or `runScan()` is called), but **scanning under jsdom is not supported**. axe-core needs a real browser — layout, computed styles, `CSSStyleSheet`, and other APIs that jsdom does not implement — so results would be incomplete or the scan would throw. Always run `runScan()` inside a browser page, as in the Playwright recipes above. ## GitHub Actions example ```yaml [.github/workflows/a11y.yml] name: A11y on: pull_request: jobs: a11y: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v6 with: version: 9 - uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build - name: Install Playwright browsers run: pnpm exec playwright install --with-deps chromium - name: Start dev server run: pnpm dev & env: CI: true - name: Wait for server run: npx wait-on http://localhost:5173 - name: Run a11y tests run: pnpm exec playwright test tests/a11y.spec.ts ``` ## Restricting to WCAG 2.1 AA only Pass axe tags as the second argument (inside `page.evaluate()` in Playwright): ```ts const { runScan } = await import("/node_modules/a11y-hud/dist/index.js"); const results = await runScan(document.body, ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]); ``` Common axe tag values: * `wcag2a` / `wcag2aa` / `wcag2aaa` — WCAG 2.0 * `wcag21a` / `wcag21aa` — WCAG 2.1 additions * `wcag22aa` — WCAG 2.2 additions * `best-practice` — non-normative best practices ## Respecting the ignore list in CI The ignore list lives in the browser's `localStorage`, which starts empty in a fresh Playwright context. If you maintain an ignore list in the HUD panel and want CI to respect it, export the list from the HUD panel as `ignores.json`, commit that file to your repo, then load it in the page before scanning: ```ts [tests/a11y.spec.ts] import ignoreList from "./ignores.json" with { type: "json" }; import { test, expect } from "@playwright/test"; test("no violations outside the shared ignore list", async ({ page }) => { await page.goto("http://localhost:5173"); const violations = await page.evaluate(async (ignoresJson) => { const { importIgnores, runScan } = await import("/node_modules/a11y-hud/dist/index.js"); importIgnores(ignoresJson); const results = await runScan(document.body); return results.violations; }, JSON.stringify(ignoreList)); expect(violations).toHaveLength(0); }); ``` `runScan()` filters ignored violations before returning, so the results already reflect the list. --- --- url: /a11y-hud/cookbook/custom-severity-colors.md --- # Custom severity colors Every color in a11y-hud is a CSS custom property. Override any token on the `a11y-hud` element to customize the look. ## Basic override ```css a11y-hud { --a11y-hud-critical: #e53e3e; --a11y-hud-serious: #dd6b20; --a11y-hud-moderate: #d69e2e; --a11y-hud-minor: #3182ce; } ``` Custom properties pierce the Shadow DOM boundary — they apply to all components inside the Shadow Root. ## Overriding on top of a built-in theme You can combine a built-in theme with individual overrides: ```js mount({ theme: "github-dark" }); ``` ```css /* Override just the critical color in github-dark */ a11y-hud { --a11y-hud-critical: #ff3b30; } ``` The override wins over the theme value because CSS specificity applies to custom properties at cascade time. ## Building a complete custom theme Override all tokens for a full custom theme: ```css a11y-hud { /* Backgrounds */ --a11y-hud-bg: #1c1c1e; --a11y-hud-bg-elevated: #2c2c2e; --a11y-hud-bg-hover: #3a3a3c; --a11y-hud-border: #3a3a3c; /* Text */ --a11y-hud-text: #f5f5f7; --a11y-hud-text-muted: #aeaeb2; /* Severity (keep semantic family!) */ --a11y-hud-critical: #ff453a; /* red */ --a11y-hud-serious: #ff9f0a; /* orange */ --a11y-hud-moderate: #ffd60a; /* yellow */ --a11y-hud-minor: #0a84ff; /* blue */ /* Accents */ --a11y-hud-highlight: #bf5af2; /* purple — WCAG level chips */ --a11y-hud-tag: #32d74b; /* teal/green — rule-set chips */ --a11y-hud-focus-ring: #0a84ff; /* Layout */ --a11y-hud-radius: 6px; --a11y-hud-panel-width: 420px; } ``` ## Dynamic theme switching Switch the built-in theme at runtime and layer overrides: ```js const hud = mount({ theme: "auto" }); document.getElementById("switch-theme").addEventListener("change", (e) => { hud.setTheme(e.target.value); }); ``` CSS overrides stay in effect regardless of which theme is active, because custom property inheritance resolves at render time. ## Reducing the panel width ```css a11y-hud { --a11y-hud-panel-width: 300px; } ``` ## Increasing the z-index If your app has a modal or overlay with a very high z-index, push the HUD above it: ```css a11y-hud { --a11y-hud-z-index: 2147483647; /* max int */ } ``` ## Contrast checking tips When overriding severity colors, verify they meet WCAG AA (4.5:1 for text, 3:1 for UI components) against the active background tokens. Good tools for checking: * [Colour Contrast Analyser (TPGi)](https://www.tpgi.com/color-contrast-checker/) * [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) The chip text color uses `--a11y-hud-bg` (the panel background). When the active chip background is a light color (e.g., yellow for moderate), make sure `--a11y-hud-bg` has sufficient contrast against it. --- --- url: /a11y-hud/cookbook/csp-compatibility.md --- # CSP compatibility a11y-hud is a dev tool — it typically runs in environments where CSP is either absent or loosely configured. This guide covers what you need if your development or staging environment enforces a CSP. ## What a11y-hud loads | Source | When | Description | |--------|------|-------------| | `https://cdn.jsdelivr.net/npm/a11y-hud/…` | Bookmarklet, CDN script tag | UMD bundle loaded from jsDelivr | | Inline styles | Always | HUD styles via Constructable Stylesheets inside Shadow DOM | axe-core is bundled into `a11y-hud` and is never fetched separately — no additional `script-src` entry is needed for it. ## npm + bundler path (no CDN) When you import from npm (`import { mount } from "a11y-hud"`), nothing is loaded from a CDN at runtime. The bundle is served by your own dev server. **No external `script-src` or `connect-src` directives are needed.** The HUD applies styles using Constructable Stylesheets (`document.adoptedStyleSheets`), which are internal to the Shadow Root. No `` tags, no external stylesheet requests. The only CSP consideration for the npm path: ```http Content-Security-Policy: style-src 'unsafe-inline'; ``` This is needed because `CSSStyleSheet` with `replaceSync()` is classified as an inline style by some browsers. The fallback path (older Safari) uses a `