---
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.

## 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.

### 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.

## 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 `