Rule examples

September 16, 2026 · View on GitHub

Hand-authored Passed/Failed (or, for manual rules, Flagged/Not applicable) example pairs for all 132 rules, meant to feed a future rule-page docs site the way alfa.siteimprove.com/rules pages show worked examples alongside a rule's description. Companion to RULE_CATALOG.md, which carries each rule's title, WCAG mapping, applicability, and expectation — this file adds one illustrative pair per rule.

Every example was verified against the built engine (npm run build, then replayed through tests/helpers/runa11yCoreOnHtml), not just read off a fixture's .case-title label or guessed from the rule's prose. Draft content: hand-curated, not generated, and not yet checked by CI the way RULE_CATALOG.md is. Once this feeds a real generator, it should get the same treatment as the fixture-marker check (scripts/generate-fixture-markers.js): a --check script wired into CI so a new or changed rule can't silently ship without a matching example.

Manual rules (type: 'manual') are capped at cantTell/notApplicable and never return pass/fail; their pair below is Flagged (cantTell) / Not applicable instead of Passed/Failed. A few automatic rules also never reach fail in practice — each says so where it applies.

accesskeys

Flagged (cantTell)

<a href="/s1" accesskey="s">S1</a>
<a href="/s2" accesskey="s">S2</a>

Two elements share the same accesskey value ("s"), which most browsers resolve unpredictably.

Not applicable

<a href="/a" accesskey="a">A</a>
<a href="/b" accesskey="b">B</a>

Every accesskey value on the page is unique.

area-alt-present

Passed

<img src="image.png" alt="" usemap="#nav">
<map name="nav">
  <area href="/x" shape="rect" coords="0,0,40,24" alt="Go to green">
</map>

The <area> is a link inside a used map and has a non-empty alt.

Failed

<img src="image.png" alt="" usemap="#nav">
<map name="nav">
  <area href="/x" shape="rect" coords="0,0,40,24">
</map>

The <area> is a link (has href) in a used map but has no alt.

area-alt-quality

Manual, and like its siblings below, flags every applicable element with a detected text alternative for review — it isn't selective about which ones look wrong. The flagged example shows a case where review genuinely matters, not a case the rule specifically detected as bad.

Flagged (cantTell)

<img src="office-map.png" usemap="#floorplan">
<map name="floorplan">
  <area href="/rooms/12" shape="rect" coords="0,0,40,24" alt="Room 12">
</map>

The area has non-empty alt text in a used image map — worth confirming "Room 12" is accurate for that region.

Not applicable

<map name="unused">
  <area href="/rooms/12" shape="rect" coords="0,0,40,24" alt="Room 12">
</map>

No <img usemap> references this map, so the area is never hit-tested or rendered — out of scope.

aria-allowed-attr

Passed

<div role="checkbox" aria-checked="true"></div>

aria-checked is one of the states role="checkbox" supports.

Failed

<div role="checkbox" aria-valuenow="1"></div>

aria-valuenow belongs to range widgets, not role="checkbox".

aria-allowed-role

Despite being type: 'automatic', this rule never returns fail — no ACT rule and no WCAG mapping exist for it, so a disallowed role is raised for review rather than decided outright.

Passed

<nav role="navigation">...</nav>

role="navigation" is permitted on <nav> by the ARIA-in-HTML spec (it's also the element's implicit role).

Flagged (cantTell)

<nav role="tab">...</nav>

role="tab" is not one of the roles ARIA-in-HTML permits on <nav>.

aria-braille-equivalent

Passed

<button aria-braillelabel="SAVE">Save changes</button>

The braille label supplements a real accessible name ("Save changes"); non-braille assistive technology still gets a name.

Flagged (cantTell)

<button aria-braillelabel="SAVE"></button>

aria-braillelabel is the only naming mechanism present; non-braille assistive technology gets no accessible name at all.

aria-checked-state-mismatch

Flagged (cantTell)

<input type="checkbox" checked aria-checked="false">

The checkbox is actually checked, but aria-checked="false" tells assistive technology the opposite.

Not applicable

<input type="checkbox" checked aria-checked="true">

aria-checked matches the control's real checked state.

aria-conditional-attr

Passed

<input type="text" aria-invalid="true" aria-errormessage="err1">
<span id="err1">Enter a valid email address.</span>

aria-invalid="true" means aria-errormessage is actually exposed to assistive technology.

Flagged (cantTell)

<input type="text" aria-errormessage="err1">
<span id="err1">Enter a valid email address.</span>

Without aria-invalid set to a non-"false" value, aria-errormessage is never exposed — the reference is currently inert.

aria-deprecated-role

Passed

<ul role="list"><li>Item</li></ul>

role="list" is current, non-deprecated ARIA.

Flagged (cantTell)

<div role="directory"><a href="/a">Item</a></div>

role="directory" is deprecated in WAI-ARIA in favor of role="list"; still valid markup, but discouraged.

aria-hidden-body

Passed

<body>Hi</body>

The document <body> is not aria-hidden.

Failed

<body aria-hidden="true">Hi</body>

Hiding <body> removes the entire page from the accessibility tree at once.

aria-hidden-focus

Passed

<div aria-hidden="true">
  <p>Decorative text</p>
</div>

Nothing inside the hidden subtree is focusable, so hiding it from assistive tech leaves no dead focus stop.

Failed

<div aria-hidden="true">
  <a href="/x">Link</a>
</div>

The link stays in the tab order even though aria-hidden removes it from the accessibility tree.

aria-prohibited-attr

Passed

<div role="generic"></div>

role="generic" cannot be named, and none of the naming attributes are present.

Failed

<div role="generic" aria-label="Something"></div>

aria-label is set on a role that WAI-ARIA prohibits from being named.

aria-prohibited-children

Passed

<ul role="menubar">
  <li role="menuitem">File</li>
  <li role="menuitem">Edit</li>
</ul>

Both owned children are roles menubar's spec allows.

Failed

<div role="table">
  <div role="row"><div role="cell">x</div></div>
  <div role="button">Not a valid table child</div>
</div>

role="table" only allows row (or rowgroup) children; the button is a disallowed owned child.

aria-required-attr

Passed

<div role="checkbox" aria-checked="true"></div>

role="checkbox" requires aria-checked, and it's present.

Failed

<div role="checkbox"></div>

role="checkbox" requires aria-checked; it's missing entirely.

aria-required-children

Passed

<ul role="list"><li>Item</li></ul>

The list role owns at least one listitem-role child.

Flagged (cantTell)

<div role="list"></div>

An empty role="list" owns no required child role — announced as a list with no items, which is what it is, but worth a look.

aria-required-parent

Passed

<div role="tablist">
  <div role="tab">Overview</div>
</div>

role="tab" requires an ancestor (or aria-owns owner) with role="tablist", and one is present.

Failed

<nav role="tab"></nav>

role="tab" has no tablist ancestor or owner anywhere on the page.

aria-role-name-present

Passed

<div role="grid" aria-label="Orders"></div>

role="grid" requires an accessible name, and aria-label provides one.

Failed

<div role="grid"></div>

role="grid" requires an accessible name; none is present.

aria-roles-valid

Passed

<div role="button">Click</div>

"button" is a real, concrete ARIA role.

Failed

<div role="buton"></div>

"buton" isn't a defined ARIA role token (typo).

aria-text

Flagged (cantTell)

<span role="text">Some text with <a href="#">a link</a> inside it.</span>

role="text" tells assistive technology to treat this as a single flat string, but the nested link is still focusable and interactive underneath it.

Not applicable

<span role="text">Plain text with no interactive content.</span>

No focusable descendant exists inside the role="text" element.

aria-valid-attr

Despite being type: 'automatic', this rule never returns fail — an aria- attribute the spec doesn't define is inert, so nothing about the element's exposed name, role, or value actually changes.*

Passed

<button aria-label="Save">Save</button>

aria-label is a real, defined ARIA attribute.

Flagged (cantTell)

<button aria-labeledby="x">Save</button>

aria-labeledby is a typo (the real attribute is aria-labelledby) and is silently ignored by assistive technology.

aria-valid-attr-value

Passed

<div aria-hidden="true"></div>

"true" is a valid value for the boolean-typed aria-hidden.

Failed

<div aria-hidden="yes"></div>

aria-hidden is boolean-typed; "yes" isn't a valid boolean token.

autocomplete-valid

Passed

<input autocomplete="email">

"email" is a recognized autofill field-name token.

Failed

<input autocomplete="emial">

"emial" is a typo, not a recognized autofill token.

avoid-inline-spacing

Passed

<p style="letter-spacing:2px !important">Spaced text</p>

The forced letter-spacing is above the WCAG 1.4.12 minimum metric.

Failed

<p style="line-height:1.2 !important">Spaced text</p>

!important forces line-height below the 1.5 minimum, and the page's own styles can't override it.

binary-control-name-present

Passed

<div role="switch" tabindex="0" aria-checked="false" aria-label="Bluetooth"></div>

aria-label gives the switch an accessible name.

Failed

<div role="switch" tabindex="0" aria-checked="false"></div>

The switch has no name from any supported mechanism.

button-name-present

Passed

<button>Save</button>

The button's text content is its accessible name.

Failed

<button></button>

The button has no text content, aria-label, or aria-labelledby.

bypass-blocks-present

Not applicable

<body>
  <nav>Site nav</nav>
  <main>Primary content</main>
</body>

A main landmark exists — a recognized bypass mechanism, so there's nothing to flag.

Flagged (cantTell)

<body>
  <nav>Site nav</nav>
  <div>Primary content, no landmark, no heading, no skip link.</div>
</body>

No main landmark, working same-page anchor link, or visible heading was detected — a review prompt, not a fail, since the engine can't confirm from one snapshot whether the page truly lacks a bypass mechanism.

canvas-text-alternative-present

Passed

<canvas>Chart summary</canvas>

Fallback content between the tags provides a text alternative.

Failed

<canvas></canvas>

No fallback content and no accessible name.

canvas-text-alternative-quality

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<canvas width="400" height="200">Bar chart</canvas>

The fallback text "Bar chart" is present but generic — worth confirming it conveys the actual data, not just the chart type.

Not applicable

<canvas width="400" height="200"></canvas>

No fallback content, ARIA name, or title exists to review; that's canvas-text-alternative-present's failure instead.

combobox-name-present

Passed

<div role="combobox" tabindex="0" aria-label="Country"></div>

aria-label names the combobox.

Failed

<div role="combobox" tabindex="0"></div>

No accessible name from any supported mechanism.

contrast-computable

This rule is a computability gate: it never returns fail, only pass (a ratio was computed), cantTell (no ratio could be computed at all), or notApplicable.

Passed

<p style="background:#ffffff; color:#999999;">Body text</p>

Foreground and background are both flat, opaque colors, so a ratio can be computed (whether or not it later passes AA/AAA).

Flagged (cantTell)

<p style="background-image:url(texture.png); color:#000000;">Body text</p>

A background-image behind the text is not a flat color, so no contrast ratio can be computed at all.

contrast-enhanced

Passed

<p style="background:#ffffff; color:#000000;">Body text</p>

Black on white is roughly 21:1, well above the AAA 7:1 threshold.

Failed

<p style="background:#ffffff; color:#767676;">Body text</p>

Gray on white is about 4.6:1 — passes the AA minimum but fails the stricter 7:1 AAA threshold.

contrast-minimum

Passed

<p style="background:#ffffff; color:#767676;">Body text</p>

About 4.6:1, above the AA 4.5:1 minimum for normal text.

Failed

<div style="opacity:0.5; background:#ffffff;">
  <p style="color:#000000;">Body text</p>
</div>

50% ancestor (group) opacity over white composites to a flat gray around 4:1, below the AA minimum.

css-focus-indicator-suppressed

Flagged (cantTell)

<style>.reset-only:focus { outline: none; }</style>
<a class="reset-only" href="/pricing">Pricing</a>

:focus { outline: none; } removes the focus indicator, and no other rule matching this link draws a replacement.

Not applicable

<style>.reset-shadow:focus { outline: none; box-shadow: 0 0 0 3px navy; }</style>
<button class="reset-shadow" type="button">Send</button>

The same rule that removes the outline draws a box-shadow in its place.

css-hidden-focus

Flagged (cantTell)

<a href="/x" style="opacity:0;">Hidden but tabbable link</a>

The link stays in the tab order despite opacity:0 making it invisible to sighted keyboard users.

Not applicable

<a href="/x">Visible link</a>

The link is both focusable and visible — nothing to flag.

css-orientation-lock

Passed

<html>
  <head>
    <title>Page title</title>
    <style>body { background: #fff; }</style>
  </head>
  <body>Hi</body>
</html>

No orientation media query rotates the page.

Failed

<html>
  <head>
    <title>Page title</title>
    <style>
      @media (orientation: landscape) {
        body { transform: rotate(90deg); }
      }
    </style>
  </head>
  <body>Hi</body>
</html>

A landscape media query forces a 90-degree rotation, locking the page to portrait regardless of device orientation.

definition-list-children-valid

Passed

<dl>
  <dt>Term</dt>
  <dd>Definition</dd>
</dl>

<dl>'s only direct children are dt/dd.

Failed

<dl>
  <dt>Term</dt>
  <dd>Definition</dd>
  <p>oops</p>
</dl>

The <p> is a direct child of <dl> that isn't dt/dd (or an allowed wrapper).

deprecated-elements-not-used

Passed

<p>Normal content</p>

No <blink> or <marquee> element anywhere, which satisfies the rule outright — there is no separate not-applicable case.

Failed

<marquee>Breaking news</marquee>

<marquee>'s auto-scrolling has no built-in way for a user to pause, stop, or hide it.

dialog-name-present

Passed

<div role="dialog" aria-label="Confirm deletion">
  <p>Are you sure?</p>
</div>

aria-label names the dialog.

Failed

<div role="dialog">
  <p>Body text only.</p>
</div>

No accessible name; visible body text doesn't count toward a dialog's name.

dlitem-parent-valid

Passed

<dl>
  <dt>Term</dt>
  <dd>Definition</dd>
</dl>

Both dt and dd are inside a <dl> ancestor.

Failed

<div>
  <dt>orphan</dt>
</div>

<dt> has no <dl> ancestor at all.

duplicate-id

A duplicate id is a real defect under WCAG 2.0/2.1 (SC 4.1.1), but that criterion was removed in 2.2, so under this engine's default 2.2 target the same defect is coerced to cantTell (with a wcagVersionScope field) instead of fail. The failed example below targets engineOptions.wcagVersion: '2.0' to show the rule's underlying decision directly.

Passed

<div id="section-1">First</div>
<div id="section-2">Second</div>

Each element has a distinct id.

Failed (with engineOptions.wcagVersion: '2.0')

<div id="section-1">First</div>
<div id="section-1">Second</div>

The id section-1 is used by two elements in the same tree, breaking <label for>, fragment links, and getElementById.

duplicate-id-aria

Despite being type: 'automatic', this rule never returns fail — which element an aria-labelledby/aria-describedby reference resolves to first is well-defined, but whether that's the intended target depends on author intent, which markup doesn't carry.

Passed

<span id="lbl">Name</span>
<input type="text" aria-labelledby="lbl">

The id lbl is unique, so the reference resolves unambiguously.

Flagged (cantTell)

<span id="lbl">Name</span>
<span id="lbl">Duplicate</span>
<input type="text" aria-labelledby="lbl">

aria-labelledby="lbl" resolves to the first element with that id — whether that's the intended target is an authoring question the markup can't answer.

embed-text-alternative-present

Passed

<embed src="report.pdf" type="application/pdf" aria-label="Quarterly report PDF">

aria-label gives the embed an accessible name.

Failed

<embed src="report.pdf" type="application/pdf">

No accessible name and no fallback content.

embed-text-alternative-quality

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<embed src="report.pdf" type="application/pdf" title="Quarterly report PDF">

A title is present — worth confirming it identifies the document well enough for someone who can't see the embedded viewer.

Not applicable

<embed src="report.pdf" type="application/pdf">

No aria-label, resolved aria-labelledby, or title exists to review; that's embed-text-alternative-present's failure instead.

empty-heading

Flagged (cantTell)

<h2></h2>

The heading has no accessible name at all — nothing for a screen reader to announce.

Not applicable

<h2>Section title</h2>

The heading has visible text content.

empty-table-header

Flagged (cantTell)

<table><tr><th></th></tr></table>

The header cell has no text content and no accessible name.

Not applicable

<table><tr><th>Name</th></tr></table>

The header cell has visible text content.

focus-order-semantics

Flagged (cantTell)

<div role="heading" tabindex="0">Fake focusable heading</div>

A heading role has no interactive behavior, so making it tabbable is unusual and worth a look.

Not applicable

<div role="button" tabindex="0">Real custom button</div>

role="button" is an interactive role, so being tabbable is expected.

form-control-label-quality

This rule is scoped to fields that already have a visible programmatic label; a field with no visible label at all belongs to form-control-programmatic-label-present/form-control-programmatic-label-quality instead.

Flagged (cantTell)

<label>Label <input type="text" name="f1"></label>

The label text is the placeholder word "Label" itself, not a real description of the field.

Not applicable

<label for="e">Email address:</label>
<input id="e" type="text">

The label text is a genuine, descriptive name for the field.

form-control-programmatic-label-present

Passed

<label for="email">Full name</label>
<input id="email" type="text">

The <label for> association gives the input a programmatic label.

Failed

<input type="text">

No <label>, aria-label, or aria-labelledby — nothing programmatic names the field.

form-control-programmatic-label-quality

Flagged (cantTell)

<input type="text" placeholder="Email address">

The control's only name comes from placeholder, which disappears once the user starts typing.

Not applicable

<label for="email">Email address</label>
<input type="text" id="email" placeholder="name@example.com">

A persistent <label> provides the name; placeholder is just a format hint here.

form-control-single-label

Passed

<label>Name <input type="text"></label>

A single wrapping label; only one labelling mechanism applies.

Failed

<label for="address">Address</label>
<label for="address">Mailing address</label>
<input id="address" type="text">

Two separate label[for] elements both target the same control.

heading-order

Flagged (cantTell)

<h1>Title</h1>
<h3>Subsection</h3>

The heading level jumps from 1 to 3, skipping level 2.

Not applicable

<h1>Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>

Each heading level increases by at most one at a time.

heading-quality

Headings with no accessible name at all belong to empty-heading, not this rule.

Flagged (cantTell)

<h3>Heading</h3>
<p>We are open Monday through Friday from 10 to 16.</p>

The heading text is the generic placeholder word "Heading" itself, not a description of the section.

Not applicable

<h3>Opening hours</h3>
<p>We are open Monday through Friday from 10 to 16.</p>

The heading names its actual topic.

html-lang-attr-present

Passed

<html lang="en">
  <head><title>Page title</title></head>
  <body>Hi</body>
</html>

A non-empty, syntactically valid language tag is declared.

Failed

<html>
  <head><title>Page title</title></head>
  <body>Hi</body>
</html>

No lang attribute at all on <html>.

html-xml-lang-mismatch

Passed

<html lang="en-US" xml:lang="en-GB">

Primary subtags agree (en); regional subtags are allowed to differ.

Failed

<html lang="en" xml:lang="fr">

lang and xml:lang declare different primary languages.

identical-iframes-same-purpose

Despite being type: 'automatic', this rule never returns fail — frames sharing a name but resolving to different URLs can still be legitimately equivalent, and nothing in the markup settles that either way.

Passed

<iframe title="Weather widget" src="https://example.test/weather"></iframe>
<iframe title="Weather widget" src="https://example.test/weather"></iframe>

Both frames share the name "Weather widget" and embed the same resource.

Flagged (cantTell)

<iframe title="Weather widget" src="https://example.test/weather?city=nyc"></iframe>
<iframe title="Weather widget" src="https://example.test/weather?city=sf"></iframe>

Both frames share the same name but resolve to different resources — possibly two legitimately equivalent widgets, possibly a mislabeled frame; the markup alone doesn't settle it.

Flagged (cantTell)

<a href="/articles/1">Read more</a>
<a href="/articles/2">Read more</a>

Both links share the accessible name "Read more" but resolve to different destinations.

Not applicable

<a href="/articles/1">Read more: Quarterly results</a>
<a href="/articles/2">Read more: Product launch</a>

Each link's accessible name is distinct, so there's no same-name group to compare destinations within.

iframe-focusable-content

Passed

<iframe tabindex="-1" width="300" height="150" srcdoc="<p>No focusable content</p>"></iframe>

tabindex="-1" removes the frame from the tab order, and nothing inside it is focusable either.

Failed

<iframe tabindex="-1" width="300" height="150" srcdoc="<a href='/x'>Link inside</a>"></iframe>

The host frame is skipped by tabindex="-1", but the link inside it is still reachable by Tab — the browser does not propagate the negative tabindex into the embedded document.

iframe-name-present

Passed

<iframe title="Video player" src="content.html"></iframe>

The title attribute names the frame.

Failed

<iframe src="content.html"></iframe>

No title, aria-label, or aria-labelledby.

iframe-title-unique

Passed

<iframe title="Chat widget" src="content.html"></iframe>

No other frame on the page shares this title.

Failed

<iframe title="Video player" src="a.html"></iframe>
<iframe title="Video player" src="b.html"></iframe>

Two frames on the same page share the identical title despite embedding different content.

image-redundant-alt

Flagged (cantTell)

<a href="/"><img alt="Home" src="home.png">Home</a>

The image's alt text ("Home") duplicates the adjacent visible link text word for word — a screen reader announces it twice.

Not applicable

<a href="/"><img alt="Home icon" src="home.png">Home</a>

The alt text ("Home icon") differs from the adjacent text ("Home").

img-alt-decorative

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<img src="quarterly-trend.png" alt="">

Marked decorative with alt="", but a filename like "quarterly-trend" suggests this might actually be an informative chart, not decoration.

Not applicable

<button aria-label="Close"><img src="x-icon.png" alt=""></button>

The button's own aria-label already provides the accessible name; the icon's exclusion isn't this element's decorative question to review.

img-alt-present

Passed

<img src="image.png" alt="Green rectangle">

A non-empty alt is present.

Failed

<img src="image.png">

No alt attribute at all (an empty alt="" would instead pass as decorative).

img-alt-quality

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<img src="chart.png" alt="image">

Non-empty alt is present, but "image" restates the element type instead of describing what the chart shows — worth a reviewer's judgment.

Not applicable

<img src="border.png" alt="">

Empty alt marks the image decorative; that's img-alt-decorative's question, not this rule's.

input-image-alt-decorative

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<input type="image" src="submit.png" alt="" title="Submit order">

Empty alt is unusual on a functional control; a title still names it, but worth confirming the empty alt was deliberate.

Not applicable

<input type="image" src="submit.png" alt="Submit order">

Non-empty alt is input-image-alt-quality's question, not this rule's.

input-image-alt-present

Passed

<input type="image" src="search.png" alt="Search">

A non-empty alt names the image button.

Failed

<input type="image" src="search.png">

No alt attribute.

input-image-alt-quality

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<input type="image" src="submit.png" alt="Submit order">

Non-empty alt is present on this image button — worth confirming "Submit order" matches what the button actually does.

Not applicable

<input type="image" src="submit.png" alt="" title="Submit order">

Empty alt on a functional control is input-image-alt-decorative's question, not this rule's.

label-in-name

Passed

<button aria-label="Save changes">Save</button>

The accessible name ("Save changes") contains the visible text ("Save").

Failed

<button aria-label="Submit form">Save</button>

The accessible name doesn't contain the visible label text at all.

label-title-only

Flagged (cantTell)

<input title="Name">

The title attribute is the field's only naming mechanism — it shows as a tooltip, not a persistent visible label.

Not applicable

<label>Name <input title="Enter your full name"></label>

A real <label> already names the field; title is just supplementary.

landmark-banner-is-top-level

Flagged (cantTell)

<div role="navigation">
  <div role="banner">Nested banner</div>
</div>

A banner landmark nested inside another landmark no longer functions as the page's single top-level header.

Not applicable

<header>Site header</header>

The banner is not nested inside any other landmark.

landmark-complementary-is-top-level

Flagged (cantTell)

<nav aria-label="Section navigation">
  <div role="complementary">Related links</div>
</nav>

A complementary landmark nested inside a navigation landmark.

Not applicable

<aside>Related links</aside>

The complementary landmark is not nested inside any other landmark.

landmark-contentinfo-is-top-level

Flagged (cantTell)

<div role="navigation">
  <div role="contentinfo">Nested contentinfo</div>
</div>

A contentinfo landmark nested inside another landmark no longer functions as the page's single top-level footer.

Not applicable

<footer>Site footer</footer>

The contentinfo is not nested inside any other landmark.

landmark-main-is-top-level

Flagged (cantTell)

<div role="navigation">
  <main>Nested main</main>
</div>

The main landmark is nested inside a navigation landmark instead of being top-level.

Not applicable

<main>Content</main>

The main landmark is not nested inside any other landmark.

landmark-no-duplicate-banner

Flagged (cantTell)

<body>
  <header>First</header>
  <header>Second</header>
</body>

Two top-level <header> elements both resolve to the banner role.

Not applicable

<body>
  <header>Site header</header>
</body>

Only one banner landmark exists.

landmark-no-duplicate-contentinfo

Flagged (cantTell)

<body>
  <footer>First</footer>
  <footer>Second</footer>
</body>

Two top-level <footer> elements both resolve to the contentinfo role.

Not applicable

<body>
  <footer>Site footer</footer>
</body>

Only one contentinfo landmark exists.

landmark-no-duplicate-main

Flagged (cantTell)

<body>
  <main>First</main>
  <main>Second</main>
</body>

Two <main> elements are both exposed to assistive technology as main landmarks.

Not applicable

<body>
  <main>Only one</main>
</body>

Only one main landmark exists.

landmark-one-main

Flagged (cantTell)

<body>
  <header>Site</header>
  <p>Primary content, no landmark.</p>
</body>

No <main> or role="main" anywhere on the page.

Not applicable

<body>
  <main>Primary content</main>
</body>

A <main> landmark exists, so there is nothing to flag.

landmark-unique

Flagged (cantTell)

<nav aria-label="Site">First</nav>
<nav aria-label="Site">Second</nav>

Two navigation landmarks share the identical accessible name, so a screen reader user can't tell them apart in the landmarks list.

Not applicable

<nav aria-label="Primary">Primary nav</nav>
<nav aria-label="Footer">Footer nav</nav>

Each navigation landmark has a distinct accessible name.

Passed

<p>Read <a href="#" style="text-decoration:underline">this underlined link</a> for more information.</p>

The link is underlined, distinguishing it from surrounding text by more than color.

Failed

<p style="color:#222222;">Read <a href="#" style="text-decoration:none; color:#2a2a2a;">this link</a> for more information.</p>

The only difference from surrounding text is a color shift with about 1.1:1 contrast, well under the 3:1 minimum, and no underline or weight/style change.

Passed

<a href="/x">Read more</a>

The link's text content is its accessible name.

Failed

<a href="/x"></a>

No text content, aria-label, or aria-labelledby.

A link with no accessible name at all is link-name-present's concern, not this rule's.

Flagged (cantTell)

<a href="/x">Click here</a>

"Click here" is a known generic phrase that doesn't describe the link's destination out of context.

Not applicable

<a href="/x">Download the 2026 pricing guide</a>

The link text is specific and descriptive.

list-children-valid

Passed

<ul>
  <li>Apple</li>
  <li>Banana</li>
</ul>

<ul>'s only direct children are <li>.

Failed

<ul>
  <li>Elderberry</li>
  <div>wrapper</div>
</ul>

A <div> is a direct child of <ul> alongside the <li>.

listbox-name-present

Passed

<ul role="listbox" tabindex="0" aria-label="Fruit"></ul>

aria-label names the listbox.

Failed

<ul role="listbox" tabindex="0"></ul>

No accessible name from any supported mechanism.

listitem-parent-valid

Passed

<ul>
  <li>Apple</li>
</ul>

<li> is inside a <ul>.

Failed

<div>
  <li>Date</li>
</div>

<li> has no <ul>/<ol>/role="list" ancestor.

manual-review

This rule has no notApplicable branch at all — it returns cantTell unconditionally on every page, including an empty <body> — so there is only one example, not a pair.

Flagged (cantTell)

<body>
  <p>Any page content.</p>
</body>

Keyboard operability and focus order always need a person driving the page; this rule states that need on every run.

media-alternative-transcript-evidence

Not applicable

<video src="talk.mp4" controls aria-describedby="t1"></video>
<div id="t1">Transcript: Welcome to today's talk...</div>

aria-describedby binds to text explicitly identified as a transcript — strong evidence.

Flagged (cantTell)

<video src="talk.mp4" controls></video>

No transcript or text-alternative evidence was found near the media element; the engine can't confirm one doesn't exist elsewhere on the page, so this is conservative, not a fail.

Passed

<div role="menuitem" tabindex="0">Cut</div>

Visible text content is a valid name source for role="menuitem".

Failed

<div role="menuitem" tabindex="0"></div>

No name and no content.

meta-refresh-no-exceptions

Passed

<meta http-equiv="refresh" content="0; URL=/x">

A delay of exactly 0 is an immediate redirect; there's nothing to interrupt a user mid-read.

Failed

<meta http-equiv="refresh" content="30; URL=/x">

At AAA, any positive delay fails, with no exemption for a long delay.

meta-refresh-timing-absent

Passed

<meta http-equiv="refresh" content="90000; URL=/x">

A delay over 20 hours (72000s) is exempt under WCAG 2.2.1 Exception 3.

Failed

<meta http-equiv="refresh" content="30; URL=/x">

A 30-second delay refreshes the page on a timer the user cannot pause, stop, or extend.

meta-viewport-large

Flagged (cantTell)

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

user-scalable=no blocks zooming past whatever scale the page loads at, short of the AAA 500% best-practice target.

Not applicable

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">

maximum-scale=5 (500%) already meets the best-practice ceiling this rule checks toward.

meta-viewport-zoom-enabled

Passed

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">

maximum-scale of 5 (500%) still allows zooming well past the 200% WCAG 1.4.4 requires.

Failed

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

user-scalable=no disables pinch-zoom entirely.

meter-name-present

Passed

<div role="meter" aria-valuenow="80" aria-label="Disk usage"></div>

aria-label names the meter.

Failed

<div role="meter" aria-valuenow="80"></div>

No accessible name from any supported mechanism.

mouse-only-event-handlers

Flagged (cantTell)

<div onmouseover="show()" onmouseout="hide()">Hover-only tooltip trigger</div>

onmouseover/onmouseout have no onfocus/onblur counterpart, so a keyboard user can never trigger this behavior.

Not applicable

<div onmouseover="show()" onmouseout="hide()" onfocus="show()" onblur="hide()" tabindex="0">Hover + focus pair</div>

Each mouse handler is paired with a keyboard-equivalent focus handler.

nested-interactive-controls-absent

Passed

<button>Click me</button>

No nested interactive control.

Failed

<button>
  <input type="checkbox"> Accept
</button>

A <button> contains a nested <input type="checkbox">, another focusable control.

no-autoplay-audio

Flagged (cantTell)

<audio autoplay src="x.mp3"></audio>

The audio autoplays unmuted with no controls attribute, giving the user no way to pause or stop it.

Not applicable

<audio autoplay controls src="x.mp3"></audio>

Native controls gives the user a pause/volume mechanism.

object-text-alternative-present

Passed

<object data="chart.svg" type="image/svg+xml">Sales chart for 2024</object>

Fallback content between the tags counts as a text alternative.

Failed

<object data="chart.svg" type="image/svg+xml"></object>

No fallback content and no accessible name.

object-text-alternative-quality

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<object data="brochure.pdf" type="application/pdf">Product brochure (PDF)</object>

Fallback text is present — worth confirming "Product brochure (PDF)" is genuinely equivalent to the embedded document, not just a filler caption.

Not applicable

<object data="brochure.pdf" type="application/pdf"></object>

No fallback content, ARIA name, or title exists to review; that's object-text-alternative-present's failure instead.

option-name-present

Passed

<div role="option" tabindex="0">Apple</div>

Visible text content is a valid name source for role="option".

Failed

<div role="option" tabindex="0"></div>

No name and no content.

p-as-heading

Flagged (cantTell)

<style>.fakeHeading { font-weight: bold; font-size: 22px; }</style>
<p class="fakeHeading">Section Title</p>

Bold, heading-sized, and short — visually a heading, but marked up as a plain <p> with no heading semantics.

Not applicable

<p>Just a normal paragraph of body text that is not bold at all.</p>

Normal weight and size; nothing visually suggests a heading.

page-has-heading-one

Flagged (cantTell)

<body>
  <h2>Section</h2>
  <p>No top-level heading anywhere on the page.</p>
</body>

No <h1> or role="heading" aria-level="1" exists.

Not applicable

<body>
  <h1>Page title</h1>
</body>

A level-one heading exists, so there is nothing to flag.

page-title-patterns

Flagged (cantTell)

<title>Home</title>

"Home" is one of the conservative generic-title signals this rule reviews.

Not applicable

<title>Quarterly Sales Report — Q3 2026</title>

The title is specific to the page's content, matching none of the low-descriptiveness patterns.

page-title-present

Passed

<html>
  <head><title>Quarterly report</title></head>
  <body>Hi</body>
</html>

The document has a non-empty <title>.

Failed

<html>
  <head></head>
  <body>Hi</body>
</html>

No <title> element anywhere in the document.

password-paste-enabled

This rule never fails — every case is either raised for review or left alone. An addEventListener-attached paste handler leaves no trace in markup, so it can't be demonstrated in a static example.

Flagged (cantTell)

<label for="p">Password</label>
<input type="password" id="p" onpaste="return false">

The inline handler cancels paste outright, blocking a password manager or a copied one-time code.

Not applicable

<label for="p">Password</label>
<input type="password" id="p" autocomplete="current-password">

No paste handler is attached at all.

presentation-role-conflict

Flagged (cantTell)

<div role="presentation" aria-label="Conflicting">Content</div>

role="presentation" asks assistive technology to skip this element, but aria-label supplies a name — per WAI-ARIA conflict resolution, the global attribute wins and the presentation role is dropped, so the element (and its name) come back into the tree unexpectedly.

Not applicable

<div role="presentation">Decorative</div>

No conflicting naming attribute is present alongside the presentation role.

presentational-children-focusable-absent

Passed

<button>Save</button>

No presentational content that also happens to be focusable.

Failed

<button>
  Save
  <span role="button" aria-label="save options" aria-expanded="false" tabindex="0">▼</span>
</button>

The nested <span> carries its own interactive role and tab stop inside the button's presentational content.

progressbar-name-present

Passed

<div role="progressbar" aria-valuenow="50" aria-label="Upload progress"></div>

aria-label names the progress bar.

Failed

<div role="progressbar" aria-valuenow="50"></div>

No accessible name from any supported mechanism.

region

Flagged (cantTell)

<body>
  <p>Stray paragraph, not inside any landmark.</p>
</body>

This paragraph sits directly under <body>, outside every landmark.

Not applicable

<body>
  <main>
    <p>Everything lives inside a landmark.</p>
  </main>
</body>

All page content is contained within a landmark region.

role-img-text-alternative-present

Passed

<span role="img" aria-label="Notifications"></span>

aria-label provides the text alternative.

Failed

<span role="img">notifications</span>

Text content is not a valid name source for an element with an explicit role="img".

scope-attr-valid

Flagged (cantTell)

<table><tr><th scope="column">Name</th></tr></table>

"column" is not a valid scope value (the correct token is "col").

Not applicable

<table><tr><th scope="col">Name</th></tr></table>

"col" is a valid scope value.

scrollable-region-focusable

Flagged (cantTell)

<style>.scrollY { overflow-y: auto; height: 100px; }</style>
<div class="scrollY">Plain long text content with no interactive children at all.</div>

The region can scroll, but it has no tabindex of its own and contains no focusable descendant, so a keyboard user can never reach it to scroll it.

Not applicable

<style>.scrollY { overflow-y: auto; height: 100px; }</style>
<div class="scrollY" tabindex="0">Long content, but the region itself is focusable.</div>

tabindex="0" makes the scrollable region itself reachable by keyboard.

searchbox-name-present

Passed

<div role="searchbox" tabindex="0" aria-label="Search the site"></div>

aria-label names the searchbox.

Failed

<div role="searchbox" tabindex="0"></div>

No accessible name from any supported mechanism.

server-side-image-map-absent

Passed

<img src="image.png" usemap="#nav" alt="Site map">
<map name="nav">
  <area href="/x" shape="rect" coords="0,0,10,10" alt="Go">
</map>

A client-side image map has no ismap attribute, which satisfies the rule outright — there is no separate not-applicable case.

Failed

<a href="/map-handler">
  <img src="image.png" ismap alt="Site map">
</a>

ismap depends on the browser sending click coordinates to the server — there's no keyboard-operable equivalent.

Flagged (cantTell)

<a href="#missing">Skip to main content</a>

The link's fragment target doesn't exist anywhere on the page, so activating it goes nowhere.

Not applicable

<a href="#target">Skip to content</a>
<div id="target">Target content</div>

The fragment target exists and is usable.

slider-name-present

Passed

<input type="range" aria-label="Volume">

aria-label names the native range input acting as a slider.

Failed

<div role="slider" tabindex="0" aria-valuenow="5" aria-valuemin="0" aria-valuemax="10"></div>

No accessible name from any supported mechanism.

spinbutton-name-present

Passed

<div role="spinbutton" tabindex="0" aria-label="Quantity"></div>

aria-label names the spinbutton.

Failed

<div role="spinbutton" tabindex="0"></div>

No accessible name from any supported mechanism.

summary-name-present

Passed

<details>
  <summary>More details</summary>
  <p>Body</p>
</details>

Visible text content names the <summary>.

Failed

<details>
  <summary></summary>
  <p>Body</p>
</details>

No text content and no accessible name.

svg-image-text-alternative-present

Passed

<svg width="40" height="24">
  <image href="logo.png"><title>Logo</title></image>
</svg>

A <title> inside the SVG <image> provides the alternative.

Failed

<svg width="40" height="24" aria-label="container">
  <image href="logo.png"></image>
</svg>

The <image> itself has no <title>/<desc>/ARIA name (the outer <svg>'s label doesn't transfer to it).

svg-text-alternative-present

Passed

<svg role="img"><title>Close</title></svg>

<title> provides the accessible name for an SVG with an explicit img role.

Failed

<svg role="img"></svg>

No <title>, <desc>, or ARIA name.

svg-text-alternative-quality

Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.

Flagged (cantTell)

<svg role="img" viewBox="0 0 24 24"><title>Growth chart</title><path d="M0 0h24v24H0z"/></svg>

A <title> is present — worth confirming "Growth chart" describes what the chart actually shows, not just that it's a chart.

Not applicable

<svg role="img" viewBox="0 0 24 24"><path d="M0 0h24v24H0z"/></svg>

No <title>/<desc> text, aria-label, or resolved aria-labelledby exists to review; that's svg-text-alternative-present's failure instead.

tab-name-present

Passed

<div role="tab" tabindex="0">Apple</div>

Visible text content names the tab.

Failed

<div role="tab" tabindex="0"></div>

No name and no content.

tabindex

Flagged (cantTell)

<div tabindex="3">Reordered</div>

A positive tabindex pulls this element out of the document's natural tab order and ahead of everything else, which usually surprises keyboard users.

Not applicable

<div tabindex="0">Focusable</div>

tabindex="0" places the element in the natural tab order without reordering anything.

table-duplicate-name

Flagged (cantTell)

<table summary="Sales data">
  <caption>Sales data</caption>
  <tr><td>1</td></tr>
</table>

The summary attribute repeats the caption text word for word instead of adding anything new.

Not applicable

<table summary="Data collected in fiscal year 2025">
  <caption>Sales data</caption>
  <tr><td>1</td></tr>
</table>

summary provides information the caption doesn't.

table-fake-caption

Flagged (cantTell)

<table>
  <tr><td>Monthly Sales Report</td></tr>
  <tr><td>Jan</td><td>100</td></tr>
  <tr><td>Feb</td><td>200</td></tr>
</table>

A lone cell sits alone in the first row above rows that all have multiple cells, and there's no real <caption> — it looks like a caption pretending to be a data cell.

Not applicable

<table>
  <caption>Monthly Sales Report</caption>
  <tr><td>Title cell</td></tr>
  <tr><td>Jan</td><td>100</td></tr>
</table>

The table has a real <caption> element.

table-headers-attr-valid

Passed

<table>
  <tr><th id="h1">Name</th></tr>
  <tr><td headers="h1">Apple</td></tr>
</table>

headers="h1" resolves to a real cell in the same table.

Failed

<table>
  <tr><th id="h2">Name</th></tr>
  <tr><td headers="missing">Banana</td></tr>
</table>

headers references an id that doesn't exist in the table.

table-th-has-data-cells

Passed

<table>
  <tr><th>Name</th></tr>
  <tr><td>Apple</td></tr>
</table>

The table has at least one <td> for the <th> to head.

Failed

<table>
  <tr><th>Name</th></tr>
</table>

The table has a <th> but zero <td> anywhere.

target-size-minimum

Confirmed against the engine's own geometry-testing harness (patchTargetSizeEnv), which encodes element size/position as a data-rect attribute rather than real CSS layout (JSDOM doesn't render). The CSS below is what a real browser would need to reproduce the same result; it isn't lifted from the fixture's own markup.

Passed

<button style="width:24px; height:24px;">+</button>

Meets the 24×24 CSS pixel minimum on its own.

Failed

<button style="width:10px; height:10px;">A</button>
<button style="width:10px; height:10px; margin-left:5px;">B</button>

Both targets are under 24×24 and closer together than the spacing exception allows.

td-has-header

Passed

<table>
  <tr><th></th><th>Q1</th><th>Q2</th><th>Q3</th></tr>
  <tr><th>North</th><td>1</td><td>2</td><td>3</td></tr>
  <tr><th>South</th><td>4</td><td>5</td><td>6</td></tr>
  <tr><th>East</th><td>7</td><td>8</td><td>9</td></tr>
</table>

Every <td> has both an implicit row header (<th> earlier in its row) and column header (<th> earlier in its column). The 4x4 minimum size isn't optional — this rule is inapplicable to smaller tables.

Failed

<table>
  <tr><td>a</td><td>b</td><td>c</td><td>d</td></tr>
  <tr><td>e</td><td>f</td><td>g</td><td>h</td></tr>
  <tr><td>i</td><td>j</td><td>k</td><td>l</td></tr>
  <tr><td>m</td><td>n</td><td>o</td><td>p</td></tr>
</table>

A 4x4 table with no <th> anywhere — no cell has a row, column, or headers-attribute association.

textbox-name-present

Passed

<div role="textbox" tabindex="0" aria-label="Search the site"></div>

aria-label names the textbox.

Failed

<div role="textbox" tabindex="0"></div>

No accessible name from any supported mechanism.

tooltip-name-present

Passed

<div role="tooltip">Click to save your changes</div>

Visible text content names the tooltip.

Failed

<div role="tooltip"></div>

No text content and no accessible name.

treeitem-name-present

Passed

<div role="treeitem" tabindex="0">Apple</div>

Visible text content names the tree item.

Failed

<div role="treeitem" tabindex="0"></div>

No name and no content.

valid-lang

Passed

<p lang="fr">Bonjour</p>

"fr" is a syntactically valid BCP 47 language tag.

Failed

<p lang="xyz123!!">???</p>

The value isn't valid BCP 47 syntax.

video-caption

Flagged (cantTell)

<video src="x.mp4"></video>

No <track> element at all — no evidence of captions or subtitles.

Not applicable

<video src="x.mp4"><track kind="captions" src="cap.vtt"></video>

A kind="captions" track with a real src is present.

video-poster-text-alternative-present

Passed

<video poster="poster.png" aria-label="Product demo"></video>

aria-label names the video element that carries the poster image.

Failed

<video poster="poster.png"></video>

No accessible name and no fallback; a poster attribute alone isn't a text alternative.