# Axe Rules

> Load this skill when configuring axe-core scans, reviewing automated accessibility test results, or writing tests that use @axe-core/playwright, @axe-core/react, or similar integrations. Provides a quick reference to axe 4.x rule IDs, their WCAG mapping, and default severity levels.

- Skill: `mgifford/axe-rules` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add mgifford/axe-rules`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mgifford/axe-rules/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: mgifford (https://skillmd.com/u/mgifford)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mgifford/axe-rules

---


# Axe-Core Rules Reference Skill

> **Canonical source**: `examples/AXE_RULES_REFERENCE.md` and `examples/AXE_RULES_COVERAGE.md` in `mgifford/ACCESSIBILITY.md`
> This skill is derived from those files. When in doubt, the example is authoritative.
> **Note:** the canonical files are organized around a specific test-fixture scanner
> project (numbered test pages). This skill re-presents the same rule set and
> severities as a general-purpose reference, independent of any particular fixture.

Apply these rules when writing, reviewing, or debugging axe-core scans.

---

## Core Principle

Axe-core catches ~30–40% of WCAG issues automatically. It is the baseline,
not the ceiling. Always pair automated scanning with manual keyboard and screen
reader testing.

**An axe-core result is evidence, not a conformance decision.** Preserve the
rule's raw outcome (`violations`, `incomplete`, `passes`, `inapplicable`)
exactly as axe reports it — do not overwrite or discard it when converting
to a project finding. Cite this file, `AXE_RULES_REFERENCE.md`, or the
rule's own `dequeuniversity.com/rules/axe/` page as the authority for a
WCAG mapping; do not invent a mapping axe-core does not document for that
rule. Record an unreviewed `violations` result as
`evidence_status: automated-indicator` with `handling: review` — never as a
confirmed standards failure — until a human confirms it. See
[Accessibility Finding Tracking: Policy Classification](https://mgifford.github.io/ACCESSIBILITY.md/examples/ACCESSIBILITY_FINDING_TRACKING.html#policy-classification).

**Key tags to include in scans:**

```typescript
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
```

Axe-core 4.11 includes 100+ automated checks across these categories: WCAG 2.0
A/AA (68 rules), WCAG 2.1 A/AA (26 rules), WCAG 2.2 A/AA (1 rule, disabled by
default), Best Practices (30+ rules), WCAG AAA (3 rules, disabled by default),
Experimental (9 rules, disabled by default), and Deprecated (7 rules, disabled
by default).

---

## Severity Scale (axe-core)

| axe severity | Meaning |
|---|---|
| **critical** | Blocks access for one or more disability groups |
| **serious** | Significantly impairs access |
| **moderate** | Creates friction; workaround exists |
| **minor** | Best-practice gap; marginal impact |

---

## Critical Rules — must never appear in production

| Rule ID | Category | Description |
|---|---|---|
| `image-alt` | Text alternatives | `<img>` missing `alt` attribute |
| `area-alt` | Text alternatives | Image map `<area>` missing alt text |
| `input-image-alt` | Text alternatives | `<input type="image">` missing alt |
| `video-caption` | Text alternatives | `<video>` without captions |
| `aria-allowed-attr` | ARIA | Element has unsupported ARIA attributes for its role |
| `aria-hidden-body` | ARIA | `aria-hidden` applied to `<body>` |
| `aria-required-attr` | ARIA | Required ARIA attribute missing |
| `aria-required-children` | ARIA | Required child ARIA roles missing |
| `aria-required-parent` | ARIA | Required parent ARIA role missing |
| `aria-roles` | ARIA | Invalid ARIA role value |
| `aria-valid-attr` | ARIA | Invalid ARIA attribute name |
| `aria-valid-attr-value` | ARIA | ARIA attribute has invalid value |
| `label` | Forms | Form input without associated `<label>` |
| `select-name` | Forms | `<select>` without accessible name |
| `button-name` | Buttons/Links | Button without discernible text |
| `input-button-name` | Buttons/Links | `<input type="button">` without text |
| `duplicate-id-aria` | Structure | Duplicate `id` values referenced by ARIA |
| `meta-refresh` | Meta | `<meta http-equiv="refresh">` causes timed redirect |

---

## Serious Rules — fix before release

| Rule ID | Category | Description |
|---|---|---|
| `object-alt` | Text alternatives | `<object>` without accessible text |
| `svg-img-alt` | Text alternatives | SVG with `role="img"` and no accessible name |
| `role-img-alt` | Text alternatives | Element with `role="img"` lacks alt text |
| `aria-braille-equivalent` | ARIA | Braille label without non-braille equivalent |
| `aria-command-name` | ARIA | Button, link, or menu item without accessible name |
| `aria-conditional-attr` | ARIA | ARIA attribute used contrary to role spec |
| `aria-hidden-focus` | ARIA | `aria-hidden` element contains focusable content |
| `aria-input-field-name` | ARIA | ARIA input field without accessible name |
| `aria-meter-name` | ARIA | ARIA `meter` element without accessible name |
| `aria-progressbar-name` | ARIA | ARIA `progressbar` element without accessible name |
| `aria-prohibited-attr` | ARIA | Prohibited ARIA attribute used |
| `aria-toggle-field-name` | ARIA | ARIA toggle field without accessible name |
| `aria-tooltip-name` | ARIA | ARIA tooltip without accessible name |
| `aria-dialog-name` | ARIA | Dialog without accessible name |
| `aria-treeitem-name` | ARIA | Tree item without accessible name |
| `color-contrast` | Color | Text contrast below 4.5:1 (normal) or 3:1 (large) |
| `frame-title` | Frames | `<iframe>` without accessible name |
| `frame-title-unique` | Frames | Frame titles are not unique |
| `frame-focusable-content` | Frames | Frame with focusable content is `tabindex="-1"` |
| `html-has-lang` | Language | `<html>` missing `lang` attribute |
| `html-lang-valid` | Language | `lang` attribute has invalid BCP 47 value |
| `valid-lang` | Language | Inline `lang` value is invalid |
| `document-title` | Language | `<title>` element missing |
| `list` | Structure | `<ul>`/`<ol>` contains elements other than `<li>` |
| `listitem` | Structure | `<li>` outside `<ul>` or `<ol>` |
| `definition-list` | Structure | `<dl>` contains elements other than `<dt>`/`<dd>` |
| `dlitem` | Structure | `<dt>`/`<dd>` outside `<dl>` |
| `bypass` | Keyboard | No skip navigation mechanism present |
| `scrollable-region-focusable` | Keyboard | Scrollable region not keyboard accessible |
| `link-name` | Buttons/Links | Link without discernible text |
| `link-in-text-block` | Buttons/Links | Link not distinguishable from surrounding text |
| `avoid-inline-spacing` | Content sizing | Text spacing overrides not adjustable |
| `autocomplete-valid` | Forms | `autocomplete` attribute invalid or missing |
| `tabindex` | Best practices | `tabindex` value > 0 used |
| `label-title-only` | Best practices | Form field labeled only via `title` attribute |
| `label-content-name-mismatch` | Best practices (experimental) | Visible label text doesn't match accessible name |
| `td-has-header` | Tables (experimental) | Large table cells lack associated headers |
| `table-fake-caption` | Tables (experimental) | Table caption not marked up with `<caption>` |
| `p-as-heading` | Best practices (experimental) | Paragraph styled to look like a heading |

---

## Moderate Rules — fix in near-term backlog

| Rule ID | Category | Description |
|---|---|---|
| `heading-order` | Headings | Heading levels skipped (e.g., h1 → h3) |
| `empty-heading` | Headings | Heading has no discernible text |
| `th-has-data-cells` | Tables | Table headers have no associated data cells |
| `td-headers-attr` | Tables | Table cells reference invalid header IDs |
| `meta-viewport` | Meta | Viewport `user-scalable=no` disables zoom |
| `html-xml-lang-mismatch` | Language | `lang` and `xml:lang` attributes disagree |
| `form-field-multiple-labels` | Forms | Form field has multiple conflicting labels |
| `landmark-one-main` | Best practices | Page has no `<main>` landmark |
| `landmark-banner-is-top-level` | Best practices | `banner` landmark not at top level |
| `landmark-contentinfo-is-top-level` | Best practices | `contentinfo` landmark not at top level |
| `landmark-main-is-top-level` | Best practices | `main` landmark not at top level |
| `landmark-no-duplicate-banner` | Best practices | More than one `banner` landmark |
| `landmark-no-duplicate-contentinfo` | Best practices | More than one `contentinfo` landmark |
| `landmark-no-duplicate-main` | Best practices | More than one `main` landmark |
| `region` | Best practices | Content exists outside landmark regions |
| `page-has-heading-one` | Best practices | Page has no `<h1>` |
| `scope-attr-valid` | Best practices | `scope` attribute has invalid value |
| `accesskeys` | Best practices | `accesskey` values are not unique |

---

## Minor Rules

| Rule ID | Category | Description |
|---|---|---|
| `table-duplicate-name` | Tables | `<caption>` text duplicates `summary` attribute |
| `aria-deprecated-role` | ARIA | Deprecated ARIA role used |
| `server-side-image-map` | Keyboard | Server-side image map used |
| `aria-text` | ARIA (best practice) | `role="text"` on an element with focusable descendants |

Disabled-by-default rules (deprecated `marquee`/`blink`/`audio-caption`) are not
listed above — flag them if encountered, but they are not enabled in a default scan.

The three WCAG AAA rules axe-core disables by default report against a
level the project's AA target does not require. If enabled and a project
has not declared a broader target, classify a confirmed finding from one of
these rules as `obligation: aspirational`, not `advisory` — it is still a
real AAA-level finding, just not part of the AA baseline. Do not report
satisfying one of these rules as WCAG AAA conformance.

---

## Running axe-core in CI (Playwright)

```typescript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('No WCAG 2.x AA violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});
```

**Common configuration options:**

```typescript
// Exclude a specific element (use sparingly — document the reason)
.exclude('#known-false-positive')

// Include only a specific element
.include('#component-under-test')

// Disable a specific rule (document the reason and JIRA/issue link)
.disableRules(['color-contrast'])
```

---

## Definition of Done Checklist

* [ ] axe-core scans run on every PR with `wcag2a`, `wcag2aa`, `wcag21aa`, `wcag22aa` tags
* [ ] Zero critical violations in production code
* [ ] Zero serious violations in production code (or explicit waiver with issue link)
* [ ] All rule exclusions documented with reason and associated issue
* [ ] Light and dark colour schemes both tested
* [ ] Mobile viewport tested
* [ ] axe results published as CI artifact for reference

---

## Key WCAG Criteria (automation coverage)

* 1.1.1 Non-text Content (A)
* 1.3.1 Info and Relationships (A)
* 1.4.3 Contrast Minimum (AA)
* 2.4.2 Page Titled (A)
* 3.1.1 Language of Page (A)
* 4.1.2 Name, Role, Value (A)
* 4.1.3 Status Messages (AA) — partial coverage

> Automation covers ~30–40% of WCAG issues. Always pair with manual testing.

---

## References

* [Full axe rules reference](https://github.com/mgifford/ACCESSIBILITY.md/blob/main/examples/AXE_RULES_REFERENCE.md)
* [Axe rules coverage summary](https://github.com/mgifford/ACCESSIBILITY.md/blob/main/examples/AXE_RULES_COVERAGE.md)
* [axe-core rule descriptions](https://dequeuniversity.com/rules/axe/)
* [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright)
* [axe-core GitHub](https://github.com/dequelabs/axe-core)

> **Standards horizon:** These rules target WCAG 2.2 AA.
> Monitor: <https://www.w3.org/TR/wcag-3.0/>

