Skip to main content

Categorized Rule Catalog

This is the live view of qa-catalog.json — the machine-readable classification that sits alongside patterns.json and feeds the axe-a11y-context harness. Every Must Have and Don't across the published web/react patterns is tagged with the verification technique(s) that can actually confirm it:

  • Static — checkable from source (a linter / AST rule).
  • LLM eval — semantic judgment on the code (a review agent).
  • Runtime — observable only when rendered and exercised (Playwright / DOM assertions).

Filter by technique to see what each layer is responsible for, or expand a pattern to read its rules and the hint / question / assertion behind each one. Consumers can read the raw JSON directly to build their own harness.

27patterns
452classified rules
catalog 0.20.0revision

Each rule is tagged with the technique(s) that can verify it. Expand a pattern to see its rules; each rule shows the linter hint, LLM question, or runtime assertion behind it.

accordion.basic11 rules8 Static6 Runtime

Must Haves

  • StaticAccordion is composed of a series of header + panel pairs.
    Static hint
    Each header control has exactly one corresponding panel element, and each panel exactly one header.
  • StaticEach accordion header control is a native `<button>` (or `role="button"` only when a native button cannot be used).
    Static hint
    Each header control is a native <button>; role="button" is the fallback path only.
  • StaticRuntimeIf role="button" is used instead of a native `<button>`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the control.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticEach header button is contained within a heading element (`<h2>`–`<h6>`) or an element with `role="heading"` and the appropriate `aria-level`.
    Static hint
    Each header button's parent is a heading element, or an element with role="heading" carrying aria-level.
  • StaticRuntimeThe header button uses `aria-expanded="true"` when its panel is visible and `"false"` when hidden.
    Static hint
    Each header button carries aria-expanded bound to its panel's visibility state.
    Runtime check
    Expand and collapse each panel; assert aria-expanded matches the panel's rendered visibility every time.
  • StaticRuntimeThe panel is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that hidden content cannot be accessed by screen readers.
    Static hint
    Collapsed panels use the hidden attribute or are unmounted; flag CSS-only hiding such as height:0 or visibility that leaves content in the accessibility tree.
    Runtime check
    With a panel collapsed, assert its content is absent from the accessibility tree and cannot be reached by keyboard.
  • RuntimeUsers move focus between accordion headers using Tab / Shift+Tab.
    Runtime check
    Tab and Shift+Tab through the accordion; assert focus stops on each header control in both directions.
  • RuntimeSince the header control is a button, it is activated with Enter or Space.
    Runtime check
    Focus a header and press Enter, then Space; assert each toggles its panel.
  • StaticIf the accordion does not permit a panel to be collapsed while expanded, the expanded header button uses `aria-disabled="true"` (rare case).
    Static hint
    In an always-one-open accordion, the expanded header carries aria-disabled="true" rather than the native disabled attribute.

Don'ts

  • RuntimeDo not remove panel content from the DOM in a way that breaks expected focus behavior (e.g., collapsing a panel while focus remains inside it without moving focus).
    Runtime check
    Move focus inside an expanded panel and collapse it; assert focus moves to a sensible element (typically its header) rather than being lost to <body>.
  • StaticDo not nest accordions within accordion panels.
    Static hint
    Flag an accordion rendered inside another accordion's panel.
button.basic9 rules8 Static2 LLM eval3 Runtime

Must Haves

  • StaticUse a native `<button>` for built-in semantics and keyboard behavior.
    Static hint
    Control is a native <button>; a div/span with role="button" is the documented fallback and only valid where a native button cannot be used.
  • StaticRuntimeIf `role="button"` is used instead of a native `<button>`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the control.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticLLM evalThe button has an accessible name that describes its purpose or action.
    Static hint
    Button has a non-empty accessible name source (text content, aria-label, or aria-labelledby).
    LLM eval question
    Does the accessible name describe what the button does, rather than naming the icon or being generic ("Click here", "Submit")?
  • StaticRuntimeWhen the button has visible text, the visible text serves as the accessible name.
    Static hint
    No aria-label or aria-labelledby overrides the button's visible text content.
    Runtime check
    Compute the accessible name; assert it equals the visible text content.
  • StaticLLM evalWhen additional context is needed beyond the visible text, add it via `aria-label`, `aria-labelledby`, or offscreen text.
    Static hint
    Added context uses aria-label, aria-labelledby, or offscreen text — not title, and not text removed from the accessibility tree.
    LLM eval question
    Is the visible text alone ambiguous among similar buttons on the screen, and if so, has distinguishing context been added?
  • RuntimeThe visible text appears at the start of the accessible name.
    Runtime check
    Compute the accessible name; assert it begins with the button's visible text.
  • StaticFor icon-only buttons, provide an accessible name using `aria-label` or `aria-labelledby`.
    Static hint
    A button whose only content is an icon carries aria-label or aria-labelledby.
  • StaticIcons within buttons must be decorative (`aria-hidden="true"`).
    Static hint
    Icon elements inside a button carry aria-hidden="true" and contribute no accessible text.
  • StaticIf the action is unavailable, disable the button using the native `disabled` attribute.
    Static hint
    Unavailable state uses the native disabled attribute, not aria-disabled paired with a click guard.
button.toggle12 rules9 Static3 LLM eval5 Runtime

Must Haves

  • StaticUse a native `<button>` element for built-in semantics and keyboard behavior.
    Static hint
    Control is a native <button>, not a div/span with role="button".
  • StaticLLM evalThe button has an accessible name that describes its purpose or action.
    Static hint
    Button has a non-empty accessible name source (text content, aria-label, or aria-labelledby).
    LLM eval question
    Does the accessible name describe what the button does, rather than being generic or naming the icon?
  • RuntimeLLM evalDefault strategy: represent state by changing the accessible name to the next action (e.g., "Mute" ↔ "Unmute", "Pin" ↔ "Remove pin").
    LLM eval question
    Does the changed name express the next action the user can take, rather than the current state?
    Runtime check
    Activate the toggle; assert the computed accessible name changes between the two states.
  • StaticRuntimeWhen the button has visible text, the visible text serves as the accessible name.
    Static hint
    No aria-label or aria-labelledby overrides the button's visible text content.
    Runtime check
    Compute the accessible name; assert it equals the visible text content.
  • StaticLLM evalWhen additional context is needed beyond the visible text, add it via `aria-label`, `aria-labelledby`, or offscreen text.
    Static hint
    Any added context uses aria-label, aria-labelledby, or offscreen text — not title or a visually hidden hack that removes it from the accessibility tree.
    LLM eval question
    Is the visible text alone ambiguous among similar toggles on the screen, and if so, has distinguishing context been added?
  • RuntimeThe visible text appears at the start of the accessible name.
    Runtime check
    Compute the accessible name; assert it begins with the button's visible text.
  • StaticFor icon-only buttons, provide an accessible name using `aria-label` or `aria-labelledby`.
    Static hint
    A button whose only child is an icon carries aria-label or aria-labelledby.
  • StaticIcons within buttons must be decorative (`aria-hidden="true"`).
    Static hint
    Icon elements inside the button carry aria-hidden="true" and contribute no accessible text.
  • StaticIf the action is unavailable, disable the button using the native `disabled` attribute.
    Static hint
    Unavailable state uses the native disabled attribute, not aria-disabled with a click guard.
  • StaticRuntimeIf the control is a formatting toggle in a toolbar (e.g., Bold/Italic/Underline), use aria-pressed="true|false" to reflect whether formatting is currently applied.
    Static hint
    Toolbar formatting toggles carry aria-pressed bound to the applied-formatting state.
    Runtime check
    Apply and remove the formatting; assert aria-pressed tracks whether the formatting is currently applied.
  • RuntimeIn this toolbar case, keep the accessible name stable (e.g., "Bold") and do not rename it to "Remove bold" or "Unbold".
    Runtime check
    Toggle the toolbar control; assert its computed accessible name is unchanged between states.

Don'ts

  • StaticDo not use `aria-pressed` for non-toolbar toggles if you are already changing the accessible name to the next action.
    Static hint
    Flag a control that both binds aria-pressed to state and computes its name/label from that same state — the two state models conflict.
carousel.dots10 rules8 Static6 Runtime

Must Haves

  • StaticRender a carousel container with `aria-roledescription="carousel"` and an accessible name (`aria-label`).
    Static hint
    The carousel container carries aria-roledescription="carousel" and a non-empty aria-label.
  • StaticEnsure the carousel container has a semantic HTML5 element or role, such as `<section>` or `role="region"`.
    Static hint
    The carousel container is a <section> or carries role="region".
  • StaticRuntimeEach slide must have `role="group"`, `aria-roledescription="slide"` and an `aria-label` like "1 of N" (N is total number of slides).
    Static hint
    Each slide carries role="group", aria-roledescription="slide", and an aria-label following the documented form.
    Runtime check
    Assert each slide's aria-label numbers it correctly and that the total matches the real slide count.
  • StaticRuntimeSlides that are not currently visible must not be rendered, or must be hidden in the DOM (e.g., via the `hidden` attribute), so their content cannot be reached by keyboard or screen readers.
    Static hint
    Off-screen slides are unmounted or carry the hidden attribute; flag CSS-only hiding such as opacity or transform.
    Runtime check
    With a slide off-screen, assert its content cannot be reached by keyboard and is absent from the accessibility tree.
  • StaticProvide Previous/Next buttons as real `<button>` elements, with `aria-label` like "Previous Slide" and "Next Slide".
    Static hint
    Previous and Next controls are <button> elements carrying accessible names.
  • StaticProvide dot navigation as real `<button>` elements in normal tab order (no roving tabindex), with `aria-label` like "Go to slide 2".
    Static hint
    Dot controls are <button> elements with accessible names and no tabindex="-1" (they stay in normal tab order).
  • StaticRuntimeSet `aria-current="true"` on the dot button corresponding to the active slide.
    Static hint
    aria-current is bound to the active-slide state on the dot controls.
    Runtime check
    Move between slides; assert exactly one dot control carries aria-current="true" and it always matches the visible slide.
  • StaticRuntimeProvide a Pause/Play button as the first focusable element inside the carousel container.
    Static hint
    A Pause/Play control is rendered as the first focusable element within the carousel container.
    Runtime check
    Tab into the carousel; assert the first control reached is the Pause/Play button.
  • RuntimeDefault to paused when `prefers-reduced-motion: reduce`.
    Runtime check
    Emulate prefers-reduced-motion: reduce and load the carousel; assert autoplay does not start.
  • RuntimePause when keyboard focus enters the carousel region.
    Runtime check
    Tab into the carousel while it is autoplaying; assert advancing stops while focus remains inside.
carousel.thumbnails10 rules8 Static6 Runtime

Must Haves

  • StaticRender a carousel container with `aria-roledescription="carousel"` and an accessible name (`aria-label`).
    Static hint
    The carousel container carries aria-roledescription="carousel" and a non-empty aria-label.
  • StaticEnsure the carousel container has a semantic HTML5 element or role, such as `<section>` or `role="region"`.
    Static hint
    The carousel container is a <section> or carries role="region".
  • StaticRuntimeEach slide must have `role="group"`, `aria-roledescription="slide"` and an `aria-label` like "1 of N".
    Static hint
    Each slide carries role="group", aria-roledescription="slide", and an aria-label following the documented form.
    Runtime check
    Assert each slide's aria-label numbers it correctly and that the total matches the real slide count.
  • StaticRuntimeSlides that are not currently visible must not be rendered, or must be hidden in the DOM (e.g., via the `hidden` attribute), so their content cannot be reached by keyboard or screen readers.
    Static hint
    Off-screen slides are unmounted or carry the hidden attribute; flag CSS-only hiding such as opacity or transform.
    Runtime check
    With a slide off-screen, assert its content cannot be reached by keyboard and is absent from the accessibility tree.
  • StaticProvide Previous/Next buttons as real `<button>` elements, with `aria-label` like "Previous Slide" and "Next Slide".
    Static hint
    Previous and Next controls are <button> elements carrying accessible names.
  • StaticProvide thumbnail navigation as real `<button>` elements in normal tab order (no roving tabindex), with `aria-label` like "Go to slide 2: {title of slide 2}".
    Static hint
    Thumbnail controls are <button> elements with accessible names and no tabindex="-1" (they stay in normal tab order).
  • StaticRuntimeSet `aria-current="true"` on the thumbnail button corresponding to the active slide.
    Static hint
    aria-current is bound to the active-slide state on the thumbnail controls.
    Runtime check
    Move between slides; assert exactly one thumbnail control carries aria-current="true" and it always matches the visible slide.
  • StaticRuntimeProvide a Pause/Play button as the first focusable element inside the carousel container.
    Static hint
    A Pause/Play control is rendered as the first focusable element within the carousel container.
    Runtime check
    Tab into the carousel; assert the first control reached is the Pause/Play button.
  • RuntimeDefault to paused when prefers-reduced-motion: reduce.
    Runtime check
    Emulate prefers-reduced-motion: reduce and load the carousel; assert autoplay does not start.
  • RuntimePause when keyboard focus enters the carousel region.
    Runtime check
    Tab into the carousel while it is autoplaying; assert advancing stops while focus remains inside.
checkbox.basic21 rules14 Static3 LLM eval12 Runtime

Must Haves

  • StaticUse a native `<input type="checkbox">` (preferred). Use a custom `role="checkbox"` only when a native input cannot be used; it then needs `tabindex="0"`, `aria-checked="true|false"` reflecting state, and a key handler where Space toggles it and prevents page scrolling.
    Static hint
    Control is an input[type="checkbox"]; otherwise it is a role="checkbox" element carrying tabindex="0", aria-checked bound to state, and a keydown handler for Space that calls preventDefault. Flag a div/span with only a click handler.
  • StaticRuntimeAssociate a visible `<label>` with the input (wrap the input, or point `for` / `htmlFor` at its `id`).
    Static hint
    Input is wrapped in a <label>, or a <label for> / htmlFor points at the input's id.
    Runtime check
    Assert the label renders visible and its text resolves into the input's computed accessible name.
  • LLM evalWord the label so it reads true when checked (e.g., "Remember me on this device").
    LLM eval question
    Does the label read as a true statement when the box is checked, rather than as an instruction or a question?
  • RuntimeThe checkbox has an accessible name equivalent to its visible label.
    Runtime check
    Compute the checkbox's accessible name; assert it matches the visible label text.
  • StaticRuntimeWhen additional context is needed, add it via `aria-label`, `aria-labelledby`, or offscreen text (`.sr-only`), with the visible text at the start of the accessible name.
    Static hint
    Added context uses aria-label, aria-labelledby, or an .sr-only element — not title or display:none text.
    Runtime check
    Compute the accessible name; assert it begins with the visible label text.
  • StaticRuntimeConvey the checked state programmatically: on a native input use the `checked` property (never `aria-checked`); on `role="checkbox"` use `aria-checked="true|false"`.
    Static hint
    Exactly one state mechanism: native input uses checked and carries no aria-checked; role="checkbox" carries aria-checked. Flag an element with both.
    Runtime check
    Toggle the control; assert the checked property (native) or aria-checked (custom) tracks the rendered state after each toggle.
  • StaticRuntimeThe control is in the tab order.
    Static hint
    Control is a native input, or a role="checkbox" element with tabindex="0"; no tabindex="-1".
    Runtime check
    Tab to the control; assert it receives focus (catches an inert or visually-hidden-with-display-none ancestor).
  • RuntimeSpace toggles the control.
    Runtime check
    Focus the checkbox and press Space; assert the state toggles and the page does not scroll.
  • RuntimeEnter does not toggle a checkbox (within a form, Enter submits).
    Runtime check
    Focus the checkbox and press Enter; assert the checked state is unchanged.
  • StaticMark it with the native `required` attribute and a visible non-color text indicator (e.g., "(required)").
    Static hint
    Required checkbox carries the native required attribute, and a text indicator such as "(required)" is rendered in or beside the label.
  • Static`aria-hidden` the visible indicator when it would duplicate the native required announcement.
    Static hint
    The visible "(required)" indicator carries aria-hidden="true" when the input already has the native required attribute.
  • StaticRuntimeGive it an inline error container that is always present in the DOM (empty until invalid).
    Static hint
    The error container is rendered unconditionally with its text as the only conditional part. Flag a container behind a conditional render (e.g., {error && <p…>}).
    Runtime check
    With the field valid, assert the error container is present in the DOM and empty.
  • StaticThe inline error container carries `aria-live="polite"`.
    Static hint
    The error container element carries aria-live="polite".
  • StaticRuntimeThe inline error container is referenced by the input's `aria-describedby`.
    Static hint
    The input's aria-describedby includes the error container's id, and is present whether or not the field is currently invalid.
    Runtime check
    Assert the aria-describedby IDREF resolves to the rendered error container.
  • RuntimePopulate the error on blur when left invalid, not on submit and not per keystroke.
    Runtime check
    Focus the required checkbox, leave it unchecked, and blur; assert the error populates. Assert it does not populate while the control still has focus, and does not wait for submit.
  • StaticLLM evalThe error text names the problem and is itself non-color (e.g., begins with "Error:").
    Static hint
    The error container renders text content when invalid, not only a color or border change.
    LLM eval question
    Does the error text state what is wrong and how to resolve it, rather than being generic ("Invalid")?
  • StaticRuntimeAssociate any supporting hint text with the input via `aria-describedby`.
    Static hint
    Hint text elements are referenced by the input's aria-describedby with a matching id.
    Runtime check
    Assert each aria-describedby IDREF resolves to a rendered element.
  • StaticIf the choice is unavailable, disable the input with the native `disabled` attribute.
    Static hint
    Unavailable state uses the native disabled attribute, not aria-disabled with a click guard.

Don'ts

  • StaticDo not hide the input with `display: none` or `visibility: hidden` when styling a custom box; that removes it from the tab order and disables keyboard operation.
    Static hint
    Flag display:none or visibility:hidden applied to the checkbox input; the .sr-only technique is the supported way to hide it visually.
  • RuntimeDo not move focus to the checkbox when its inline error appears; populating the `aria-live` container is what announces it.
    Runtime check
    Blur the invalid required checkbox to trigger the error; assert document.activeElement is unchanged by the error appearing.
  • LLM evalDo not use a single checkbox for a set of related options that answer one question (that is a grouped pattern).
    LLM eval question
    Do these checkboxes answer one shared question whose labels are not meaningful alone (checkbox.group), rather than each being an independent choice?
checkbox.group18 rules10 Static3 LLM eval9 Runtime

Must Haves

  • StaticWrap the related checkboxes in a single named group.
    Static hint
    The set is wrapped in a <fieldset> with a <legend> (preferred), or a container with role="group" and aria-labelledby pointing at the visible group-label element.
  • LLM evalThe group label states the question or instruction for the whole set (e.g., "Which genres do you watch?"), not a generic heading.
    LLM eval question
    Does the legend or group label state the question the set answers, rather than being a generic heading like "Options" or "Preferences"?
  • StaticRuntimeReference any group-level hint (e.g., "Select at least one") from each checkbox's `aria-describedby`.
    Static hint
    The hint element's id appears in the aria-describedby of every checkbox in the group. Flag the hint referenced only from the fieldset or role="group" container.
    Runtime check
    Assert every option's aria-describedby resolves to the rendered hint element.
  • StaticEach option is a native `<input type="checkbox">` (preferred) with a programmatically associated visible `<label>` and its own `checked` state.
    Static hint
    Every option is an input[type="checkbox"] with its own label association and independent checked binding — not a shared value.
  • RuntimeSpace toggles the focused option.
    Runtime check
    Focus an option and press Space; assert only that option's state toggles and the others are unchanged.
  • RuntimeTab and Shift+Tab move between options, and each option is its own tab stop.
    Runtime check
    Tab through the group; assert focus stops on every option in turn, in both directions (no roving tabindex skipping options).
  • StaticIndicate the requirement in the group's accessible name (e.g., append "(required)" to the `<legend>` or `aria-labelledby` target), since a group has no native `required` attribute.
    Static hint
    The legend or aria-labelledby target text carries a required indicator; the group carries no native required attribute and no aria-invalid.
  • StaticRuntimeProvide one inline error container, present in the DOM at all times (empty until invalid).
    Static hint
    Exactly one error container serves the group, rendered unconditionally. Flag a per-option error container, and flag a container behind a conditional render.
    Runtime check
    With the group valid, assert exactly one error container exists for the group and is empty.
  • StaticThe group's inline error container carries `aria-live="polite"`.
    Static hint
    The group error container carries aria-live="polite".
  • StaticRuntimeThe group's inline error container is referenced from each checkbox's `aria-describedby`.
    Static hint
    The error container id appears in every option's aria-describedby. Flag the reference placed on the <fieldset> or role="group" container instead.
    Runtime check
    Assert every option's aria-describedby resolves to the single group error container.
  • RuntimePopulate the error only when focus leaves the whole group with the requirement unmet, not as focus moves between options.
    Runtime check
    Tab through every option leaving the requirement unmet; assert no error appears while focus stays inside the group, and exactly one error appears once focus leaves it.
  • RuntimeWhile invalid, give the group a visible non-color indication (e.g., a red outline plus error text beginning with "Error:").
    Runtime check
    Drive the group invalid; assert a text indication is rendered, not only a color or border change.
  • RuntimeDo not move focus when the error appears; submit-time focus handling is a form-level concern (see `form.error-summary`).
    Runtime check
    Move focus out of the invalid group; assert document.activeElement is wherever the user moved it, unchanged by the error appearing.

Don'ts

  • StaticRuntimeDo not use `role="radiogroup"`, `role="radio"`, or arrow-key roving focus for checkboxes; checkboxes allow multiple selections, and each is a tab stop.
    Static hint
    Flag role="radiogroup" or role="radio" on the group or its options, and flag a roving-tabindex implementation (options carrying tabindex="-1").
    Runtime check
    Focus an option and press Arrow keys; assert focus does not move between options.
  • StaticDo not put `required` on every checkbox to express "select at least one"; native `required` forces that specific box to be checked.
    Static hint
    Flag the native required attribute on individual options of a group whose requirement is group-level.
  • StaticDo not rely on `aria-describedby` on the `<fieldset>` or `role="group"` container to convey the hint or error.
    Static hint
    Flag aria-describedby authored on the <fieldset> or role="group" container; the reference belongs on each <input>.
  • LLM evalDo not wrap a single, self-sufficient checkbox in a `<fieldset>` and `<legend>` (that belongs to `checkbox.basic`).
    LLM eval question
    Does this group contain a set of options answering one shared question, rather than a single self-sufficient checkbox?
  • LLM evalDo not combine unrelated questions in one group; one question per `<fieldset>` or `role="group"`.
    LLM eval question
    Do all options in this group answer the same single question?
collection-row.basic14 rules10 Static1 LLM eval9 Runtime

Must Haves

  • StaticRuntimeUse a visible heading, typically an `<h2>`, above the row.
    Static hint
    A heading element precedes the row container.
    Runtime check
    Assert the heading renders visible above the row.
  • StaticUse list semantics for the row: `ul` with `li` items.
    Static hint
    The row is a <ul> whose direct children are <li> elements.
  • StaticRuntimeEach item must comprise a single focus stop: in other words, consist of a single link `<a>`.
    Static hint
    Each <li> contains exactly one interactive element, an <a>; flag nested buttons or additional links inside an item.
    Runtime check
    Tab through the row; assert focus stops once per item.
  • StaticEach item contains a visual element (image, poster, thumbnail, or media preview).
    Static hint
    Each item link contains an image or media element.
  • StaticLLM evalEach item contains a visible title that identifies the item.
    Static hint
    Each item link renders a text title element.
    LLM eval question
    Does the visible title identify the item on its own, rather than relying on the poster art?
  • StaticRuntimeEach item link must have an accessible name composed of: title + metadata (optional) via `aria-labelledby`.
    Static hint
    Each item link carries aria-labelledby referencing its title element, and the metadata element when present.
    Runtime check
    Compute each item link's accessible name; assert it contains the visible title.
  • StaticProvide an offscreen "X of Y" element.
    Static hint
    Each item renders an offscreen element carrying position text in an "X of Y" form.
  • StaticRuntimeReference it via `aria-describedby`.
    Static hint
    Each item link's aria-describedby references its offscreen position element.
    Runtime check
    Assert each item's aria-describedby IDREF resolves to the rendered position element.
  • RuntimeThe position must reflect the item's index within the full set, not just the currently visible subset.
    Runtime check
    Page to the end of the row; assert the last item's position text reads its index within the full set (e.g., "18 of 18"), not its index among the visible items.
  • StaticNext button on the right edge of the row container (vertically centered).
    Static hint
    A Next paging control is rendered within the row container.
  • StaticRuntimePrevious button on the left edge when not on the first page.
    Static hint
    A Previous paging control is rendered within the row container.
    Runtime check
    On the first page assert the Previous control is absent or non-focusable; after paging forward assert it is present.
  • RuntimeActivating Next moves focus to the first newly revealed item (left-most visible link).
    Runtime check
    Activate Next; assert document.activeElement is the first item revealed by that page turn.
  • RuntimeActivating Previous moves focus to the last newly revealed item (right-most visible link).
    Runtime check
    Activate Previous; assert document.activeElement is the last item revealed by that page turn.

Don'ts

  • RuntimeDo not let Tab from the last visible item move into out-of-view items; it must reach the Next button instead.
    Runtime check
    Focus the last visible item and press Tab; assert focus lands on the Next button and never on an item scrolled out of view.
combobox.autocomplete23 rules11 Static19 Runtime

Must Haves

  • StaticUse a native `<input>` element with `role="combobox"` as the editable control.
    Static hint
    The editable control is an <input> carrying role="combobox".
  • StaticSet `aria-autocomplete="list"` on the input to indicate that typing filters a list of options.
    Static hint
    The input carries aria-autocomplete="list".
  • StaticRender the popup as an element with `role="listbox"` and a stable ID (the same one referenced by `aria-controls`).
    Static hint
    The popup carries role="listbox" and an id that does not change across renders.
  • StaticRuntimeGive each option `role="option"`, a stable ID, and `aria-selected="true|false"`.
    Static hint
    Each option carries role="option", a deterministic id, and aria-selected.
    Runtime check
    Select an option; assert aria-selected is true on it and false on the others.
  • StaticRuntimeProvide a visible native `<label>` associated with the input via `for`/`id`.
    Static hint
    A <label for> / htmlFor points at the input's id.
    Runtime check
    Assert the label renders visible and resolves into the input's computed accessible name.
  • StaticRuntimeSet `aria-expanded="true|false"` on the input reflecting whether the listbox is displayed.
    Static hint
    The input carries aria-expanded bound to the listbox's visibility.
    Runtime check
    Open and close the listbox; assert aria-expanded matches its rendered visibility every time.
  • StaticRuntimeSet `aria-controls="IDREF"` on the input pointing to the listbox element.
    Static hint
    The input carries aria-controls whose value matches the listbox id.
    Runtime check
    Assert the aria-controls IDREF resolves to the rendered listbox.
  • StaticRuntimeThe listbox is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that when closed, its options cannot be reached by keyboard or screen readers.
    Static hint
    The closed listbox uses the hidden attribute or is unmounted; flag CSS-only hiding.
    Runtime check
    With the listbox closed, assert its options are absent from the accessibility tree.
  • RuntimeArrow Down moves to the first option, opening the listbox first if it is closed.
    Runtime check
    With the listbox closed, press Arrow Down; assert it opens and the first option becomes active.
  • RuntimeArrow Up moves to the last option, opening the listbox first if it is closed.
    Runtime check
    With the listbox closed, press Arrow Up; assert it opens and the last option becomes active.
  • RuntimeAlt+Arrow Down opens the listbox without moving the active option.
    Runtime check
    With the listbox closed, press Alt+Arrow Down; assert it opens and no option is active.
  • RuntimeWhen the listbox is open, Arrow Up and Arrow Down move the active option and wrap at the ends.
    Runtime check
    Arrow past both ends of the open listbox; assert the active option moves and wraps.
  • RuntimeEnter sets the input value to the active option and closes the listbox.
    Runtime check
    Make an option active and press Enter; assert the input value becomes that option and the listbox closes.
  • RuntimeHome, End, Left, and Right move the caret within the input text rather than the active option.
    Runtime check
    With the listbox open, press Home, End, Left, and Right; assert the caret moves in the input and the active option is unchanged.
  • RuntimeA printable character returns focus to the input, inserts the character, and filters the listbox to matching options.
    Runtime check
    Type a printable character; assert it is inserted into the input and the listbox filters to matching options.
  • RuntimeDo not capture the keys the browser uses for text editing (character keys, caret movement, selection, and deletion).
    Runtime check
    Type, select with Shift+Arrow, and delete with Backspace inside the input; assert each behaves as in a plain text field.
  • RuntimeKeep DOM focus on the input at all times so native text editing keeps working.
    Runtime check
    Arrow through the options; assert document.activeElement remains the input throughout.
  • StaticRuntimeIndicate the active option by setting `aria-activedescendant="{optionId}"` on the input, pointing to the currently highlighted option.
    Static hint
    The input carries aria-activedescendant bound to the active option's id.
    Runtime check
    Arrow through the options; assert aria-activedescendant always resolves to the highlighted option.
  • StaticRuntimeWhen no option is active, remove `aria-activedescendant` (do not point it at a stale or nonexistent ID).
    Static hint
    aria-activedescendant is omitted rather than set to a placeholder when no option is active.
    Runtime check
    With no option active, assert the input has no aria-activedescendant, or that its value resolves to a rendered element.
  • RuntimeScroll the active option into view via scripting when it changes so it is visible.
    Runtime check
    Arrow to an option outside the scrolled viewport of the listbox; assert it is scrolled into view.
  • RuntimeThe active option has a distinct visual highlight.
    Runtime check
    Arrow through the options; assert the active option's computed style visibly differs from the inactive options.
  • RuntimeEsc closes the listbox if it is open, or clears the input if the listbox is already closed.
    Runtime check
    Press Esc with the listbox open; assert it closes. Press Esc again; assert the input is cleared.

Don'ts

  • StaticDo not use `role="menu"` or `role="menuitem"` for the options; use `role="listbox"` and `role="option"`.
    Static hint
    Flag role="menu" or role="menuitem" on the popup or its options.
dialog.basic17 rules11 Static13 Runtime

Must Haves

  • StaticUse the native `<dialog>` element (preferred). When `<dialog>` cannot be used, `<div role="dialog">` is the documented fallback.
    Static hint
    Dialog surface is a native <dialog>, or a <div role="dialog"> taking the documented fallback path.
  • StaticRuntimeOpen with `.showModal()`.
    Static hint
    The open path calls .showModal() on the dialog element; flag .show() or setting the open attribute directly.
    Runtime check
    Activate the trigger; assert the dialog is open and matches the :modal pseudo-class.
  • StaticRuntimeClose with `.close()`.
    Static hint
    The close path calls .close(); flag handlers that only unmount or conditionally render the dialog away.
    Runtime check
    Open the dialog and close it; assert it reports closed rather than being removed from the DOM while open.
  • StaticDialog surface has an accessible name via `aria-labelledby` (preferred) or `aria-label`.
    Static hint
    The dialog element carries aria-labelledby or a non-empty aria-label.
  • StaticRuntimeIf `aria-labelledby` is used, it references a visible title element (e.g., `<h2 id="...">`).
    Static hint
    The aria-labelledby IDREF matches an element authored inside the dialog.
    Runtime check
    With the dialog open, assert the referenced element exists and is visible.
  • StaticRuntimeIf a description is rendered, it is referenced by `aria-describedby`.
    Static hint
    Descriptive content inside the dialog is referenced by aria-describedby rather than left to reading order.
    Runtime check
    Assert the aria-describedby IDREF resolves to the rendered description element.
  • StaticRuntimeDialog width is fluid so content reflows at 400% zoom (WCAG 1.4.10 Reflow).
    Static hint
    The dialog surface uses a fluid max-width (e.g., min(Npx, 100%)); flag a fixed pixel width above 320px.
    Runtime check
    At a 320 CSS pixel wide viewport, open the dialog; assert no horizontal scrolling and the surface fits the viewport.
  • StaticRuntimeProvide a visible close control (`<button type="button">`) with an accessible name (e.g., `aria-label="Close dialog"`).
    Static hint
    The dialog contains a <button type="button"> close control carrying an accessible name.
    Runtime check
    With the dialog open, assert the close control is rendered visible and has a non-empty accessible name.
  • StaticThe invoking control declares dialog-trigger semantics — apply `aria-haspopup="dialog"` to the trigger element.
    Static hint
    The trigger element carries aria-haspopup="dialog".
  • RuntimeFocus moves into the dialog on open.
    Runtime check
    Activate the trigger; assert document.activeElement is the dialog or a descendant.
  • RuntimeFocus is trapped within the dialog while open.
    Runtime check
    With the dialog open, Tab and Shift+Tab past the first and last focusable elements; assert focus never reaches a background control.
  • RuntimeEscape closes the dialog.
    Runtime check
    With the dialog open, press Escape; assert the dialog closes.
  • RuntimeFocus returns to the invoking element on close.
    Runtime check
    Open and close the dialog by each supported path; assert document.activeElement is the invoking element after every close.
  • RuntimeBackground content is not focusable or reachable by keyboard or screen readers while open.
    Runtime check
    With the dialog open, Tab and programmatically focus a background control; assert focus cannot land there and the background is excluded from the accessibility tree.
  • RuntimeBody scroll is prevented while open.
    Runtime check
    On a page taller than the viewport, open the dialog and attempt to scroll; assert the scroll position does not change.

Don'ts

  • StaticRuntimeDo not render the manual fallback inside containers that create clipping or stacking contexts (e.g., `overflow: hidden/auto`, `transform`).
    Static hint
    The fallback dialog is portalled to document.body rather than rendered inside a transformed or overflow-clipped ancestor.
    Runtime check
    With the fallback open, walk its ancestors and assert none apply overflow clipping or a transform; assert the dialog renders unclipped and is the topmost hit-testable element at its own coordinates.
  • StaticDo not inert `document.body` or `document.documentElement` on the manual fallback; inert only the application content root.
    Static hint
    inert is applied to the application content root; flag inert set on body or documentElement.
disclosure.basic10 rules10 Static1 LLM eval5 Runtime

Must Haves

  • StaticUse a native `<button>` (preferred), or `role="button"` only when a native button cannot be used.
    Static hint
    Trigger is a native <button>; role="button" is the fallback path only.
  • StaticRuntimeIf `role="button"` is used instead of a native `<button>`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the control.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticRuntimeThe button reflects open state with `aria-expanded="true|false"`.
    Static hint
    The trigger carries aria-expanded bound to the region's visibility; flag aria-expanded authored on the region instead of the trigger.
    Runtime check
    Toggle the disclosure; assert aria-expanded on the trigger matches the region's rendered visibility.
  • StaticRuntimeThe button references the region with `aria-controls="IDREF"`.
    Static hint
    The trigger carries aria-controls whose value matches the region element's id.
    Runtime check
    Assert the aria-controls IDREF resolves to the rendered region.
  • StaticRuntimeThe region is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that when closed, its content cannot be reached by keyboard or screen readers.
    Static hint
    The closed region uses the hidden attribute or is unmounted; flag CSS-only hiding.
    Runtime check
    With the region closed, assert its content cannot be reached by keyboard and is absent from the accessibility tree.
  • StaticLLM evalThe button has an accessible name that describes the content it controls.
    Static hint
    Trigger has a non-empty accessible name source.
    LLM eval question
    Does the trigger's name describe the content it reveals, rather than the action alone ("Show", "More")?
  • StaticRuntimeWhen the button has visible text, the visible text serves as the accessible name.
    Static hint
    No aria-label or aria-labelledby overrides the trigger's visible text.
    Runtime check
    Compute the accessible name; assert it equals the visible text content.
  • StaticFor an icon-only trigger, provide an accessible name using `aria-label` or `aria-labelledby`.
    Static hint
    A trigger whose only content is an icon carries aria-label or aria-labelledby.
  • StaticThe button omits `aria-haspopup`.
    Static hint
    Flag aria-haspopup on the disclosure trigger — a revealed region is not a popup widget.

Don'ts

  • StaticDo not use `role="menu"` on the region.
    Static hint
    Flag role="menu" on the disclosure region.
grid.channel-guide20 rules12 Static12 Runtime

Must Haves

  • StaticUse `role="grid"` with an accessible name (e.g., `aria-label="Channel guide"`).
    Static hint
    The grid container carries role="grid" and a non-empty accessible name.
  • StaticUse `role="row"` for each row, including the header row.
    Static hint
    Every row element, header row included, carries role="row".
  • StaticUse `role="columnheader"` for the time header cells (typically static).
    Static hint
    Time header cells carry role="columnheader".
  • StaticUse `role="rowheader"` for the channel name/logo column (may be interactive).
    Static hint
    Channel column cells carry role="rowheader".
  • StaticUse `role="gridcell"` for program listing cells (interactive).
    Static hint
    Program listing cells carry role="gridcell".
  • StaticGrid structural roles (`gridcell`, `rowheader`, `columnheader`) must be applied to container elements.
    Static hint
    Flag role="gridcell", role="rowheader", or role="columnheader" applied directly to a <button> or <a>.
  • StaticInteractive controls (e.g., `<button>`, `<a>`) must be nested inside those containers.
    Static hint
    Each interactive control is a descendant of its grid structural container, not the container itself.
  • StaticRuntimeSet `aria-rowcount` to the total number of rows in the grid.
    Static hint
    The grid carries aria-rowcount.
    Runtime check
    Assert aria-rowcount equals the total row count including the header row, not the count currently rendered.
  • StaticRuntimeSet `aria-colcount` to the total number of columns in the grid (including the channel column).
    Static hint
    The grid carries aria-colcount.
    Runtime check
    Assert aria-colcount equals the total column count including the channel column, not the count currently rendered.
  • StaticRuntimeSet `aria-rowindex` on each row and `aria-colindex` on each cell (1-based, counting the header row and the channel column), so position announcements remain correct when rows or columns are rendered lazily.
    Static hint
    Rows carry aria-rowindex and cells carry aria-colindex.
    Runtime check
    Scroll the grid so lazily-rendered rows and columns mount; assert each index reflects position in the full set, 1-based, counting the header row and channel column.
  • StaticRuntimeOnly one cell is tabbable at a time (roving `tabIndex`: active cell `0`, all others `-1`).
    Static hint
    Cells carry tabIndex bound to the active-cell index — one at 0, the rest at -1.
    Runtime check
    Arrow around the grid; assert exactly one cell has tabIndex 0 at a time and it is the focused cell.
  • RuntimeArrow keys move focus within the grid (Left/Right/Up/Down).
    Runtime check
    Press each arrow key from an interior cell; assert focus moves one cell in that direction.
  • RuntimeTab/Shift+Tab exits the grid to the next/previous focusable element outside.
    Runtime check
    From a cell, press Tab and Shift+Tab; assert focus leaves the grid entirely rather than moving to another cell.
  • RuntimeChannel column (row header) opens "channel details".
    Runtime check
    Activate a row header cell; assert the channel details view opens.
  • Runtime"Now" column activates tune (no-op if already selected).
    Runtime check
    Activate a cell in the "Now" column; assert it tunes, and that activating the already-selected channel is a no-op.
  • RuntimeFuture columns open "program details" (demo can use a modal).
    Runtime check
    Activate a cell in a future time column; assert the program details view opens.
  • RuntimeClicking a cell updates the roving "current cell" so Arrow-key navigation continues from that cell.
    Runtime check
    Click a cell, then press an arrow key; assert focus moves relative to the clicked cell, not the previously active one.
  • RuntimeWhen focus leaves and re-enters the grid, focus lands on the last focused cell.
    Runtime check
    Focus an interior cell, Tab out of the grid, then Shift+Tab back in; assert focus returns to that same cell.

Don'ts

  • StaticDo not mix multiple interactive controls inside a cell in this basic pattern.
    Static hint
    Flag a grid cell containing more than one focusable control.
  • RuntimeDo not conflate "selected channel" with "focused cell".
    Runtime check
    Arrow across several cells without activating; assert the selected channel state does not change as focus moves.
link.basic12 rules10 Static2 LLM eval6 Runtime

Must Haves

  • StaticUse a native `<a>` element with an `href` whenever possible.
    Static hint
    Link is an <a> carrying a non-empty href; flag <a> without href and role="link" on other elements.
  • StaticLLM evalThe link has an accessible name that describes its purpose or destination.
    Static hint
    Link has a non-empty accessible name source.
    LLM eval question
    Does the accessible name identify where the link goes, rather than being generic ("here", "read more", "learn more")?
  • StaticRuntimeWhen the link has visible text, the visible text serves as the accessible name.
    Static hint
    No aria-label or aria-labelledby overrides the link's visible text content.
    Runtime check
    Compute the accessible name; assert it equals the visible text content.
  • StaticLLM evalWhen additional context is needed beyond the visible text, add it via `aria-label`, `aria-labelledby`, or offscreen text.
    Static hint
    Added context uses aria-label, aria-labelledby, or offscreen text.
    LLM eval question
    Would the link text be ambiguous when read out of context (e.g., in a screen reader's links list), and if so, has context been added?
  • RuntimeThe visible text appears at the start of the accessible name.
    Runtime check
    Compute the accessible name; assert it begins with the link's visible text.
  • StaticFor icon-only links, provide an accessible name using `aria-label` or `aria-labelledby`.
    Static hint
    A link whose only content is an icon carries aria-label or aria-labelledby.
  • StaticIcons within links must be decorative (`aria-hidden="true"`).
    Static hint
    Icon elements inside a link carry aria-hidden="true".
  • RuntimeKeyboard activation must follow native link behavior: Enter activates; Space does not.
    Runtime check
    Focus the link and press Enter; assert it navigates. Press Space; assert it does not activate the link.
  • StaticRuntimeIf using `role="link"` on a non-`<a>` element, you must also provide keyboard support and focus management (e.g., `tabIndex="0"` and Enter key activation).
    Static hint
    role="link" elements carry tabindex="0" and a keydown handler for Enter.
    Runtime check
    Tab to the role="link" element and press Enter; assert it receives focus and navigates.
  • StaticRuntimeIf a link opens a new tab/window, include programmatic context in the accessible name (e.g., "opens in new tab").
    Static hint
    Links with target="_blank" carry aria-label, aria-describedby, or offscreen text conveying that a new tab opens.
    Runtime check
    Compute the accessible name of each new-tab link; assert it conveys that the link opens a new tab or window.
  • StaticIf a link opens a new tab/window, include a visual affordance: append an external-link icon at the end of the visible label.
    Static hint
    Links with target="_blank" render an external-link icon element after the label text.

Don'ts

  • StaticRuntimeDo not style a link to look like plain text when it appears inline within a paragraph; inline links must be visually obvious (e.g., underlined).
    Static hint
    Flag text-decoration:none on links rendered inside running text with no other non-color distinction authored.
    Runtime check
    Compare an inline link's computed style with the surrounding text; assert a non-color difference such as an underline.
listbox.basic15 rules6 Static11 Runtime

Must Haves

  • StaticThe container is an element with `role="listbox"` and an accessible name via `aria-label` or `aria-labelledby`.
    Static hint
    The container carries role="listbox" and a non-empty aria-label or a resolving aria-labelledby.
  • StaticRuntimeEach option is an element with `role="option"` and `aria-selected="true|false"`.
    Static hint
    Each option carries role="option" and aria-selected bound to selection state.
    Runtime check
    Change the selection; assert aria-selected tracks the visible selection on every option.
  • StaticOptions contain no interactive child elements (no links, buttons, inputs, or nested controls).
    Static hint
    Flag <a>, <button>, or form controls inside a role="option" element.
  • StaticFor multiple selection, the listbox has `aria-multiselectable="true"`.
    Static hint
    A multi-select listbox carries aria-multiselectable="true".
  • RuntimeArrow Down and Arrow Up move the active option to the next and previous option.
    Runtime check
    Press Arrow Down and Arrow Up; assert the active option moves one option in each direction.
  • RuntimeHome and End move the active option to the first and last option.
    Runtime check
    Press Home and End; assert the active option becomes the first and last option.
  • RuntimeA printable character performs type-ahead, moving the active option to the next option whose label begins with the typed characters.
    Runtime check
    Type a character matching a later option's first letter; assert the active option moves to it.
  • RuntimeFor single-select, selection is operable from the keyboard alone.
    Runtime check
    Using only the keyboard, change the selection in a single-select listbox; assert aria-selected moves to the new option.
  • RuntimeSpace toggles `aria-selected` on the active option.
    Runtime check
    Press Space on the active option in a multi-select listbox; assert its aria-selected toggles and the others are unchanged.
  • RuntimeShift+Arrow Up and Shift+Arrow Down extend the selection to the adjacent option.
    Runtime check
    Press Shift+Arrow Up and Shift+Arrow Down; assert the adjacent option joins the selection.
  • RuntimeCtrl+A (Cmd+A on macOS) selects all options.
    Runtime check
    Press Ctrl+A (Cmd+A on macOS); assert every option reports aria-selected true.
  • RuntimeNo modifier-only click is required to select an option.
    Runtime check
    Click options without holding a modifier; assert multiple options can be selected.
  • StaticRuntimeManage the active option with either roving tabindex or `aria-activedescendant`, and apply one approach consistently.
    Static hint
    Either the options carry a roving tabindex (active at "0", the rest "-1"), or the listbox carries tabindex="0" plus aria-activedescendant — not a mix of both.
    Runtime check
    Move the active option; assert the chosen mechanism updates consistently and the other is never introduced.
  • RuntimeScripting keeps the active option scrolled into view.
    Runtime check
    Move the active option beyond the listbox's scrolled viewport; assert it is scrolled into view.

Don'ts

  • StaticDo not use `role="menu"` or `role="menuitem"` for a selection list.
    Static hint
    Flag role="menu" or role="menuitem" on the listbox or its options.
menu.basic28 rules17 Static2 LLM eval16 Runtime

Must Haves

  • StaticUse a native `<button>` (preferred), or `role="button"` only when a native button cannot be used.
    Static hint
    Trigger is a native <button>; role="button" is the fallback path only.
  • StaticRuntimeIf `role="button"` is used instead of a native `<button>`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the control.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticThe menu container has `role="menu"`.
    Static hint
    The menu container carries role="menu".
  • StaticEach command uses `role="menuitem"`.
    Static hint
    Each command element carries role="menuitem".
  • StaticRuntimeA stateful item that toggles in place uses `role="menuitemcheckbox"` or `role="menuitemradio"` with `aria-checked="true|false"`.
    Static hint
    Toggling items carry role="menuitemcheckbox" or role="menuitemradio" with aria-checked bound to state.
    Runtime check
    Toggle a stateful item; assert aria-checked flips and the menu behaves per its documented stay-open or close contract.
  • StaticDividers between groups of items use `role="separator"`.
    Static hint
    Divider elements carry role="separator".
  • StaticThe menu contains only menu parts (`menuitem`, `menuitemcheckbox`, `menuitemradio`, `separator`, and grouping wrappers with `role="none"`), with no links, headings, or form inputs.
    Static hint
    Flag <a>, heading elements, or form controls inside the role="menu" container.
  • StaticLLM evalThe trigger has an accessible name that describes its purpose or action.
    Static hint
    Trigger has a non-empty accessible name source.
    LLM eval question
    Does the trigger's name describe what the menu does, rather than naming the icon?
  • StaticFor an icon-only trigger, provide an accessible name using `aria-label` or `aria-labelledby` (e.g., "More actions").
    Static hint
    An icon-only trigger carries aria-label or aria-labelledby.
  • StaticIcons within the trigger must be decorative (`aria-hidden="true"`).
    Static hint
    Icon elements inside the trigger carry aria-hidden="true".
  • StaticThe menu container has an accessible name via `aria-labelledby` referencing the trigger, or via `aria-label`.
    Static hint
    The menu container carries aria-labelledby pointing at the trigger, or a non-empty aria-label.
  • StaticThe trigger has `aria-haspopup="menu"`.
    Static hint
    The trigger carries aria-haspopup="menu".
  • StaticRuntimeThe trigger has `aria-expanded="true|false"` reflecting the open or closed state of the menu.
    Static hint
    The trigger carries aria-expanded bound to the menu's visibility.
    Runtime check
    Open and close the menu; assert aria-expanded matches the menu's rendered visibility every time.
  • StaticRuntimeThe trigger has `aria-controls="IDREF"` pointing to the menu container.
    Static hint
    The trigger carries aria-controls whose value matches the menu container's id.
    Runtime check
    Assert the aria-controls IDREF resolves to the rendered menu container.
  • StaticRuntimeThe menu is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that when closed, its items cannot be reached by keyboard or screen readers.
    Static hint
    The closed menu uses the hidden attribute or is unmounted; flag CSS-only hiding.
    Runtime check
    With the menu closed, assert its items cannot be reached by keyboard and are absent from the accessibility tree.
  • RuntimeOn the trigger, Enter, Space, and Arrow Down open the menu and move focus to the first item.
    Runtime check
    Focus the trigger and press Enter, Space, and Arrow Down in turn; assert each opens the menu with focus on the first item.
  • RuntimeOn the trigger, Arrow Up opens the menu and moves focus to the last item.
    Runtime check
    Focus the trigger and press Arrow Up; assert the menu opens with focus on the last item.
  • RuntimeWithin the menu, Arrow Up and Arrow Down move focus between items and wrap at the ends.
    Runtime check
    Arrow through the open menu past both ends; assert focus moves item to item and wraps.
  • RuntimeWithin the menu, Home moves focus to the first item and End moves focus to the last item.
    Runtime check
    With the menu open, press Home and End; assert focus lands on the first and last items.
  • RuntimeWithin the menu, Enter or Space activates the focused item, then closes the menu and returns focus to the trigger.
    Runtime check
    Focus a menu item and press Enter, then repeat with Space; assert the item activates, the menu closes, and focus returns to the trigger.
  • RuntimeWithin the menu, a printable character moves focus to the next item whose label starts with that character (type-ahead).
    Runtime check
    With the menu open, type a character matching a later item's first letter; assert focus moves to that item.
  • StaticRuntimeFocus is managed with roving tabindex: the active item has `tabindex="0"` and all other items have `tabindex="-1"`.
    Static hint
    Menu items carry tabindex bound to the active index — one item at "0", the rest at "-1".
    Runtime check
    Arrow through the menu; assert exactly one item has tabindex="0" at a time and it is the focused item.
  • RuntimeThe active item receives DOM focus via `element.focus()`.
    Runtime check
    Arrow through the menu; assert document.activeElement is the active menu item, not the container.
  • RuntimeEsc closes the menu and returns focus to the trigger.
    Runtime check
    With the menu open, press Esc; assert it closes and document.activeElement is the trigger.
  • RuntimeTab moves focus out of the menu and closes it.
    Runtime check
    With the menu open, press Tab; assert the menu closes and focus moves to the next focusable element on the page.
  • RuntimeAn outside click or focus loss closes the menu.
    Runtime check
    With the menu open, click outside it and separately move focus away; assert it closes in both cases.

Don'ts

  • LLM evalDo not use `role="menu"` for navigation or for items that are links.
    LLM eval question
    Do these items perform application commands, rather than navigating to destinations (which belongs in navigation-menu.dropdown)?
  • StaticDo not reach for `aria-activedescendant` when roving tabindex is simpler for the case at hand.
    Static hint
    Flag aria-activedescendant on a menu that also manages a roving tabindex.
menu.menubar21 rules11 Static2 LLM eval12 Runtime

Must Haves

  • StaticThe container has `role="menubar"`.
    Static hint
    The container carries role="menubar".
  • StaticOnly menu-related elements appear inside the menubar (top-level items, submenus, and separators). No links, no arbitrary content.
    Static hint
    Flag <a>, headings, or form controls inside the role="menubar" container.
  • StaticEach top-level item has `role="menuitem"`.
    Static hint
    Each top-level item carries role="menuitem".
  • StaticLLM evalEach submenu is a container with `role="menu"` and an accessible name that matches its top-level item (e.g., `aria-label="File"`).
    Static hint
    Each submenu carries role="menu" and a non-empty accessible name.
    LLM eval question
    Does each submenu's accessible name match the label of the top-level item that opens it?
  • StaticSubmenu commands have `role="menuitem"`, or `role="menuitemcheckbox"` or `role="menuitemradio"` for stateful commands.
    Static hint
    Each submenu command carries role="menuitem", role="menuitemcheckbox", or role="menuitemradio".
  • StaticDividers within a submenu use `role="separator"`.
    Static hint
    Divider elements inside a submenu carry role="separator".
  • StaticThe submenu contains only menu parts (`menuitem`, `menuitemcheckbox`, `menuitemradio`, and `separator`), no links or arbitrary content.
    Static hint
    Flag <a> or arbitrary content inside a submenu role="menu" container.
  • StaticThe container has an accessible name (e.g., `aria-label="Application"`).
    Static hint
    The menubar container carries a non-empty aria-label or a resolving aria-labelledby.
  • StaticRuntimeEach top-level item that opens a submenu has `aria-haspopup="menu"` and `aria-expanded="true|false"` reflecting open/closed.
    Static hint
    Submenu-opening items carry aria-haspopup="menu" and aria-expanded bound to submenu state.
    Runtime check
    Open and close each submenu; assert its top-level item's aria-expanded matches the submenu's rendered visibility.
  • StaticRuntimeEach submenu is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that when closed, its commands cannot be reached by keyboard or screen readers.
    Static hint
    Closed submenus use the hidden attribute or are unmounted; flag CSS-only hiding.
    Runtime check
    With a submenu closed, assert its commands are absent from the accessibility tree.
  • RuntimeArrow Left and Arrow Right move focus between top-level items and wrap at the ends.
    Runtime check
    Press Arrow Left and Arrow Right past both ends; assert focus moves between top-level items and wraps.
  • RuntimeArrow Down, Enter, or Space on a top-level item opens its submenu and moves focus to the first submenu item.
    Runtime check
    On a top-level item press Arrow Down, Enter, and Space in turn; assert each opens the submenu with focus on its first command.
  • RuntimeWithin an open submenu, Arrow Up and Arrow Down move focus between commands and wrap at the ends.
    Runtime check
    Arrow through an open submenu past both ends; assert focus moves between commands and wraps.
  • RuntimeWithin an open submenu, Arrow Right moves to the adjacent top-level menu, and Arrow Left moves to the previous top-level menu (opening that menu).
    Runtime check
    From inside an open submenu press Arrow Right, then Arrow Left; assert focus moves to the adjacent top-level menu and opens it.
  • RuntimeWithin an open submenu, Home moves focus to the first command and End moves focus to the last command.
    Runtime check
    With a submenu open, press Home and End; assert focus lands on the first and last commands.
  • RuntimeEnter or Space activates the focused submenu command.
    Runtime check
    Focus a submenu command and press Enter, then Space; assert each activates it.
  • RuntimeType-ahead moves focus to the next item whose label starts with the typed character, both across top-level items and within an open submenu.
    Runtime check
    Type a matching character on the menubar and again inside an open submenu; assert focus moves to the matching item in both contexts.
  • StaticRuntimeFocus is managed with roving tabindex across the top-level items.
    Static hint
    Top-level items carry a roving tabindex — the first at "0" and the rest at "-1" on load.
    Runtime check
    Move between top-level items; assert exactly one carries tabindex="0" at a time and it is the focused item.
  • RuntimeEsc closes the current submenu and returns focus to its top-level item.
    Runtime check
    With a submenu open, press Esc; assert it closes and focus returns to its top-level item.
  • RuntimeThe menubar closes any open submenu when focus moves outside the menubar (e.g., via Tab or an outside pointer click), so no submenu stays visible while its top-level item is at `aria-expanded="false"`.
    Runtime check
    With a submenu open, Tab out of the menubar and separately click outside it; assert the submenu closes in both cases.

Don'ts

  • LLM evalDo not use a menubar for site or app navigation; a horizontal bar of links is navigation and must use `navigation-menu.basic`, not menu roles.
    LLM eval question
    Do these top-level items invoke application commands, rather than navigating to destinations (which belongs in navigation-menu.basic)?
navigation-menu.basic30 rules19 Static1 LLM eval18 Runtime

Must Haves

  • StaticNavigation is contained within a `<nav>` landmark, or an element with `role="navigation"`, and an accessible name (`aria-label`) when multiple nav landmarks exist.
    Static hint
    The navigation is inside a <nav> or role="navigation"; when the page has more than one, each carries a distinguishing aria-label.
  • StaticTop-level navigation items are presented in a list structure (e.g., `<ul><li>…</li></ul>`).
    Static hint
    Top-level items are <li> children of a <ul>.
  • StaticEach top-level item is one of the following: a simple link, a parent link with a separate menu toggle button, or a parent button.
    Static hint
    Each top-level item matches one of the three documented shapes; flag a shape that mixes them, such as a single element acting as both link and toggle.
  • StaticThe toggle control is a native `<button>` (preferred) or `role="button"` only when a native button cannot be used.
    Static hint
    Submenu toggles are native <button> elements; role="button" is the fallback path only.
  • StaticRuntimeIf using `role="button"`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating.
    Static hint
    role="button" toggles carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus a role="button" toggle and press Enter, then Space; assert each opens the submenu and Space does not scroll the page.
  • StaticThe sub-menu container is positioned immediately after its toggle control in the DOM.
    Static hint
    Each submenu container is the next sibling of its toggle control.
  • StaticSub-menu items are links and/or buttons contained in a list structure (`<ul><li>…</li></ul>`).
    Static hint
    Submenu items are links or buttons inside <li> children of a <ul>.
  • StaticLLM evalIf the toggle is an icon button associated with a parent link (case B), it has an accessible name that includes the parent link text.
    Static hint
    Icon-only toggles carry aria-label or aria-labelledby.
    LLM eval question
    Does the toggle's accessible name include its parent link's text, so it is distinguishable from other toggles in the nav?
  • StaticRuntimeIf the top-level item is a link, apply aria-current="page" to that link.
    Static hint
    The top-level link matching the current route carries aria-current="page".
    Runtime check
    Navigate to a top-level destination; assert exactly one link in the nav carries aria-current="page" and it is that one.
  • StaticIf the top-level item is a button-only parent (no destination), do not put aria-current on the button. Instead, apply aria-current="page" to the submenu link that represents the current page.
    Static hint
    Flag aria-current on a button-only parent; the attribute belongs on the submenu link for the current page.
  • StaticRuntimeIf the navigation has no explicit Home link and the site logo links to the homepage, the logo link carries `aria-current="page"` when the user is on the homepage.
    Static hint
    Where no Home link exists, the logo link is bound to carry aria-current="page" on the homepage route.
    Runtime check
    Load the homepage; assert the logo link carries aria-current="page".
  • RuntimeEnsure there is some visual change that indicates this is the current page link.
    Runtime check
    Compare the current-page link's computed style with its siblings; assert a visible difference beyond the accessible-state attribute.
  • StaticRuntimeThe toggle reflects open state with `aria-expanded="true|false"`.
    Static hint
    Each toggle carries aria-expanded bound to its submenu's visibility.
    Runtime check
    Open and close each submenu; assert aria-expanded matches its rendered visibility every time.
  • StaticRuntimeThe toggle references its submenu with `aria-controls="IDREF"` (recommended).
    Static hint
    Each toggle carries aria-controls whose value matches its submenu container's id.
    Runtime check
    Assert each aria-controls IDREF resolves to the rendered submenu container.
  • StaticThe toggle does not carry `aria-haspopup`; a submenu of links is not a menu, listbox, tree, grid, or dialog, so `aria-expanded` alone is the correct state signal.
    Static hint
    Flag aria-haspopup on a navigation submenu toggle.
  • StaticRuntimeTop-level items that open a sub-menu include a visible indicator (e.g., down caret).
    Static hint
    Submenu-opening items render an indicator element alongside the label.
    Runtime check
    Assert the indicator renders visible on every top-level item that opens a submenu.
  • StaticThe indicator is decorative and does not replace the accessible name of the toggle (e.g., use an aria-label like "Categories menu" on icon-only toggles).
    Static hint
    The indicator carries aria-hidden="true" and the toggle has its own accessible name.
  • RuntimeSub-menus all default to closed, or hidden, when the page loads.
    Runtime check
    On first load, assert every submenu is hidden and every toggle reports aria-expanded="false".
  • StaticRuntimeThe sub-menu is shown/hidden in the DOM (e.g., via `hidden`) so that when closed, submenu links cannot be reached by keyboard or screen readers.
    Static hint
    Closed submenus use the hidden attribute or are unmounted; flag CSS-only hiding.
    Runtime check
    With a submenu closed, assert its links cannot be reached by keyboard and are absent from the accessibility tree.
  • RuntimeIf focus is on the last focusable element in an open submenu and the user presses Tab, the submenu closes and focus moves to the next focusable element after the submenu.
    Runtime check
    Focus the last item in an open submenu and press Tab; assert the submenu closes and focus lands after it.
  • RuntimeIf focus is on the first focusable element in an open submenu and the user presses Shift+Tab, the submenu closes and focus moves to the toggle control.
    Runtime check
    Focus the first item in an open submenu and press Shift+Tab; assert the submenu closes and focus returns to the toggle.
  • RuntimeThe sub-menu is non-modal and does not trap focus.
    Runtime check
    With a submenu open, Tab past its last item; assert focus continues into the rest of the page.
  • RuntimeWhen a submenu is opened, focus remains on the toggle control.
    Runtime check
    Open a submenu; assert document.activeElement is still the toggle.
  • RuntimeWhen focus moves outside the toggle + submenu (Tab away or click elsewhere), the submenu closes.
    Runtime check
    Open a submenu, then move focus outside it by Tab and by an outside click; assert it closes in both cases.
  • RuntimeEsc closes an open submenu and returns focus to its toggle control.
    Runtime check
    With a submenu open, press Esc; assert it closes and focus returns to its toggle.
  • RuntimeIf the navigation contains multiple sub-menus, opening one closes any other open sub-menu.
    Runtime check
    Open one submenu, then open a second; assert the first closes.

Don'ts

  • RuntimeDo not open sub-menus on hover only.
    Runtime check
    Without any pointer interaction, focus a toggle and activate it by keyboard; assert the submenu opens.
  • StaticDo not make a top-level item both a navigation link and the submenu toggle using the same element.
    Static hint
    Flag a single element carrying both an href and the submenu toggle handler.
  • StaticDo not use `role="menu"` / `role="menuitem"` unless you implement the full ARIA menu widget behavior (managed focus, arrow keys, typeahead).
    Static hint
    Flag role="menu" or role="menuitem" in a navigation menu built from links.
  • RuntimeDo not strand focus by removing the currently focused submenu item without closing and allowing focus to move naturally.
    Runtime check
    Close a submenu while focus is on one of its items; assert focus moves to the toggle rather than being lost to <body>.
navigation-menu.dropdown18 rules11 Static2 LLM eval10 Runtime

Must Haves

  • StaticUse a native `<button>` (preferred), or `role="button"` only when a native button cannot be used.
    Static hint
    Trigger is a native <button>; role="button" is the fallback path only.
  • StaticRuntimeIf `role="button"` is used instead of a native `<button>`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the control.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticRuntimeThe list is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that when closed, its content cannot be reached by keyboard or screen readers.
    Static hint
    The closed list uses the hidden attribute or is unmounted; flag CSS-only hiding.
    Runtime check
    With the dropdown closed, assert its links cannot be reached by keyboard and are absent from the accessibility tree.
  • StaticItems are contained in a list structure (`<ul><li>…</li></ul>`) holding links and an optional trailing action `<button>`.
    Static hint
    Dropdown items are <li> children of a <ul>, containing links and at most one trailing <button>.
  • StaticLLM evalThe button has an accessible name that describes its purpose or action.
    Static hint
    Trigger has a non-empty accessible name source.
    LLM eval question
    Does the trigger's name describe what the dropdown contains or does, rather than naming the avatar or icon?
  • StaticFor an icon-only avatar button, provide an accessible name using `aria-label` or `aria-labelledby`.
    Static hint
    An avatar-only trigger carries aria-label or aria-labelledby.
  • StaticIcons within the button are decorative (`aria-hidden="true"`).
    Static hint
    Icon and avatar image elements inside the trigger carry aria-hidden="true" or an empty alt.
  • StaticRuntimeThe button reflects open state with `aria-expanded="true|false"`.
    Static hint
    The trigger carries aria-expanded bound to the list's visibility.
    Runtime check
    Open and close the dropdown; assert aria-expanded matches the list's rendered visibility every time.
  • StaticRuntimeThe button is associated with the list container via `aria-controls="IDREF"`.
    Static hint
    The trigger carries aria-controls whose value matches the list container's id.
    Runtime check
    Assert the aria-controls IDREF resolves to the rendered list container.
  • StaticThe button does not carry `aria-haspopup`.
    Static hint
    Flag aria-haspopup on the trigger — a list of links is not a menu, listbox, tree, grid, or dialog.
  • RuntimeWhen the dropdown opens, focus remains on the invoking button.
    Runtime check
    Open the dropdown; assert document.activeElement is still the trigger.
  • RuntimeThe dropdown does not trap focus. Users can Tab through the items and continue to the rest of the page.
    Runtime check
    With the dropdown open, Tab past the last item; assert focus continues to the next focusable element on the page.
  • RuntimeWhen focus moves outside the button and list (Tab away, click elsewhere), the dropdown closes.
    Runtime check
    Open the dropdown, then move focus outside it by Tab and by an outside click; assert it closes in both cases.
  • RuntimeEsc closes the dropdown and returns focus to the invoking button (use `requestAnimationFrame` for the focus restore).
    Runtime check
    With the dropdown open, press Esc; assert it closes and document.activeElement is the invoking button.

Don'ts

  • StaticDo not use `role="menu"` or `role="menuitem"`; the items are links in a list, not menu commands.
    Static hint
    Flag role="menu" or role="menuitem" on the dropdown container or its items.
  • RuntimeDo not make hover or pointer click the only way to open the dropdown; opening must also work with keyboard Enter and Space.
    Runtime check
    Focus the trigger and press Enter, then Space; assert each opens the dropdown without any pointer interaction.
  • RuntimeDo not close the dropdown in a way that strands focus (e.g., removing the focused element without moving focus).
    Runtime check
    Close the dropdown while focus is on one of its items; assert focus moves to the trigger rather than being lost to <body>.
  • LLM evalDo not let a hidden dropdown be the only path to a destination; expose those links elsewhere on the site as well.
    LLM eval question
    Is every destination in this dropdown reachable somewhere else on the site, such as a footer or sitemap?
popover.basic12 rules8 Static1 LLM eval8 Runtime

Must Haves

  • StaticThe popover surface uses `role="dialog"`, or a native `<dialog>` element shown non-modally (rendered in the DOM or opened with `.show()`, never `.showModal()`).
    Static hint
    The surface carries role="dialog", or is a native <dialog> opened with .show(); flag any .showModal() call on it.
  • StaticRuntimeThe popover has an accessible name via `aria-labelledby` (preferred), referencing a visible title element (e.g., `<h2 id="...">`), or `aria-label`.
    Static hint
    The popover carries aria-labelledby or a non-empty aria-label.
    Runtime check
    With the popover open, assert its computed accessible name is non-empty and any aria-labelledby IDREF resolves to a visible element.
  • RuntimeFocus moves into the popover on open, landing on the popover container or its first interactive control.
    Runtime check
    Open the popover; assert document.activeElement is the popover container or a descendant.
  • RuntimeEsc closes the popover, regardless of where focus currently sits, since focus is not trapped and may have moved into the page.
    Runtime check
    Open the popover, Tab focus out into the page, then press Esc; assert the popover closes.
  • RuntimeFocus is restored to the invoking element on close.
    Runtime check
    Open and close the popover; assert document.activeElement is the invoking element.
  • StaticCapture the invoking element at open time, and restore focus with `requestAnimationFrame` after the popover is unmounted.
    Static hint
    The open path stores a reference to the invoker, and the close path restores focus inside a requestAnimationFrame callback.
  • RuntimeThe popover does not trap focus. Tab and Shift+Tab move out of the popover into the rest of the page, which stays interactive.
    Runtime check
    With the popover open, Tab past its last focusable element; assert focus continues into the page and background controls remain operable.
  • StaticRuntimeThe popover does not set `aria-modal="true"` and does not apply `inert` to the background.
    Static hint
    Flag aria-modal on the popover, and flag inert applied to the app root or body when it opens.
    Runtime check
    With the popover open, assert background elements are focusable and have no inert ancestor.
  • StaticRuntimeProvide a visible close control. Use a native `<button>` (preferred), or `role="button"` only when a native button cannot be used.
    Static hint
    The popover contains a close control that is a native <button>, or role="button" on the fallback path.
    Runtime check
    With the popover open, assert the close control renders visible.
  • StaticRuntimeIf `role="button"` is used instead of a native `<button>`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the control.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticLLM evalThe close control has an accessible name that describes its purpose or action (e.g., `aria-label="Close"`).
    Static hint
    The close control has a non-empty accessible name source.
    LLM eval question
    Does the close control's name describe the action it performs, rather than being an icon name or empty?

Don'ts

  • StaticDo not bind Esc only to a container-level handler on the popover surface; once focus leaves the untrapped popover, that handler stops firing and Esc no longer closes.
    Static hint
    The Esc handler is registered at the document level, not only as a React onKeyDown on the popover surface.
progress-bar.basic23 rules16 Static4 LLM eval11 Runtime

Must Haves

  • StaticUse the native `<progress>` element (preferred), or `role="progressbar"` on a non-native element only when the native element cannot be used.
    Static hint
    The indicator is a native <progress>, or a non-native element carrying role="progressbar" on the fallback path.
  • StaticRuntimeThe progress bar is not focusable and is not in the page tab sequence.
    Static hint
    The progress bar carries no tabindex and is not a natively focusable element.
    Runtime check
    Tab through the page; assert focus never lands on the progress bar.
  • StaticThe `<progress>` element, or the element carrying `role="progressbar"`, contains no text, headings, images, or interactive content.
    Static hint
    Flag any element content inside <progress> or the role="progressbar" element — descendants of the role are presentational.
  • StaticRender the label, the numeric value, and any status message as siblings of the progress bar.
    Static hint
    Label, value, and status elements are siblings of the progress bar, not descendants.
  • StaticRuntimeWhen the progress bar reports loading of a specific region of the page, that region has `aria-busy="true"` until loading completes, and the attribute is removed or set to `"false"` afterward.
    Static hint
    The loading region carries aria-busy bound to the in-flight state.
    Runtime check
    Start and finish a load; assert aria-busy is "true" during and absent or "false" after.
  • StaticLLM evalThe progress bar has an accessible name that describes what is progressing, via `<label>` or `aria-labelledby` referencing visible text when it exists, otherwise via `aria-label`.
    Static hint
    The progress bar has a name source: an associated <label>, aria-labelledby, or aria-label.
    LLM eval question
    Does the name say what is progressing, rather than being generic ("Progress", "Loading")?
  • RuntimeWhen a visible label exists, the visible text is contained in the accessible name and appears at the start of it.
    Runtime check
    Compute the accessible name; assert it begins with the visible label text.
  • StaticRuntimeWhen context beyond the name is needed, add it via `aria-describedby` referencing the visible text that carries it.
    Static hint
    Supplementary text is referenced by aria-describedby rather than folded into the name.
    Runtime check
    Assert the aria-describedby IDREF resolves to the rendered text element.
  • StaticRuntimeA determinate progress bar sets `aria-valuenow` to the current value, updated as the process advances.
    Static hint
    A determinate bar carries aria-valuenow bound to state; on <progress> the value attribute carries this and aria-valuenow is not added.
    Runtime check
    Advance the process; assert the reported value tracks it at each step.
  • Static`aria-valuemin` and `aria-valuemax` are set when the range is not 0 to 100, and omitted otherwise, since `aria-valuemin` defaults to `0` and `aria-valuemax` defaults to `100`.
    Static hint
    aria-valuemin and aria-valuemax appear only on bars whose range is not 0–100.
  • StaticAn indeterminate progress bar omits `aria-valuenow` entirely.
    Static hint
    An indeterminate bar carries no aria-valuenow, not even "0"; on <progress> the value attribute is omitted.
  • StaticLLM evalWhen a percentage does not describe the value accurately, set `aria-valuetext` to the unit the user cares about (e.g., "3 of 8 files", "About 2 minutes remaining").
    Static hint
    aria-valuetext is present where the raw percentage is not the meaningful unit.
    LLM eval question
    Is a bare percentage a poor description of this value, and if so does aria-valuetext give the unit the user cares about?
  • RuntimeThe filled portion of the bar and the unfilled track differ from each other by a contrast ratio of at least 3:1, per `global.non-text-contrast`.
    Runtime check
    Compute the contrast between the fill and the track colors; assert at least 3:1.
  • StaticUnder Windows High Contrast Mode, author background colors are overridden, so pair the styling with a `@media (forced-colors: active)` override that gives the track a `1px` border in `CanvasText` and the fill a background in `Highlight`.
    Static hint
    A @media (forced-colors: active) block gives the track a border in CanvasText and the fill a background in Highlight.
  • StaticRuntimeStatus messages tied to the progress bar (e.g., "Uploading", "Almost done", "Complete") are rendered into a live region that is present in the DOM before the first message appears. Use `role="status"`, or an equivalent such as `aria-live="polite"` and `aria-atomic="true"`.
    Static hint
    The live region is rendered unconditionally and carries role="status", or aria-live="polite" with aria-atomic="true".
    Runtime check
    Before any status message appears, assert the live region already exists in the DOM.
  • StaticWhen those messages are visible on screen, the visible container is the live region, and no second copy is added.
    Static hint
    Where visible status text exists, that element is the live region; flag a second visually hidden region carrying the same text.
  • StaticRuntimeWhen the design has no visible status messages, a determinate progress bar announces its completion once through a visually hidden live region (e.g., "Upload complete"), per `global.sr-only`.
    Static hint
    Where no visible status text exists, a visually hidden live region carries the completion message.
    Runtime check
    Run the process to completion; assert the completion message is announced exactly once.
  • LLM evalAnnouncement text takes its words from the progress bar's accessible name rather than newly authored wording, so the announcement matches the visible label (e.g., a bar named "Uploading episode files" gives "Upload complete").
    LLM eval question
    Does the announcement borrow its words from the progress bar's accessible name, rather than introducing new wording the user never sees?
  • RuntimeText in a visually hidden live region is cleared once it no longer describes the current state, so a stale message is not discoverable later by a user browsing the page.
    Runtime check
    After completion, assert the hidden live region is empty rather than retaining the last message.
  • RuntimeAnnouncements are limited to the status messages the design defines and to completion. Intermediate value changes are not announced.
    Runtime check
    Advance the value through many steps; assert the live region updates only at the defined points, not on every tick.
  • RuntimeUnder `prefers-reduced-motion: reduce`, the progress bar runs no looping animation, such as a barber-pole stripe or a fill that sweeps back and forth, and stays on screen as a static indicator of ongoing work.
    Runtime check
    Emulate prefers-reduced-motion: reduce; assert no looping animation runs and the bar remains rendered.

Don'ts

  • StaticDo not give the progress bar element `aria-live`, and do not nest it inside a live region. Every value change is then announced, interrupting the user's reading and navigation.
    Static hint
    Flag aria-live on the progress bar, and flag a progress bar rendered inside a live-region ancestor.
  • LLM evalDo not use `role="progressbar"` for a value the user can change.
    LLM eval question
    Does the user only observe this value, rather than adjusting it (which would be a slider or spinbutton)?
select.basic24 rules17 Static17 Runtime

Must Haves

  • StaticRuntimeProvide a visible label for the field. Prefer a native `<label>` and ensure the custom UI is programmatically associated with the label (see `aria-labelledby` below).
    Static hint
    A visible label element exists and is referenced by the custom control's aria-labelledby.
    Runtime check
    Assert the label renders visible and contributes to the button's computed accessible name.
  • StaticInclude a visually hidden native `<select>` (required).
    Static hint
    A native <select> is rendered and visually hidden with the .sr-only technique rather than display:none.
  • RuntimeThe hidden `<select>` includes the same options and current value as the custom UI.
    Runtime check
    Change the selection in the custom UI; assert the hidden select's value and option set match.
  • StaticThe hidden `<select>` includes form attributes as needed (e.g., `name`, `required`, `disabled`).
    Static hint
    The hidden select carries name, and required/disabled where the field needs them.
  • StaticRuntimeThe hidden `<select>` is not focusable (so users don't tab to both controls).
    Static hint
    The hidden select carries tabindex="-1" or aria-hidden="true".
    Runtime check
    Tab through the field; assert focus stops once, on the visible button, never on the hidden select.
  • StaticThe visible interactive control is a native `<button>` (preferred) or `role="button"` only when a native button cannot be used.
    Static hint
    The trigger is a native <button>; role="button" is the fallback path only.
  • StaticRuntimeIf using `role="button"`, add `tabindex="0"` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating.
    Static hint
    role="button" elements carry tabindex="0" and a keydown handler covering Enter and Space that calls preventDefault for Space.
    Runtime check
    Focus the role="button" control and press Enter, then Space; assert each activates it and that Space does not scroll the page.
  • StaticThe button carries `aria-haspopup="listbox"`.
    Static hint
    The trigger carries aria-haspopup="listbox".
  • StaticRuntimeThe button carries `aria-expanded="true|false"` reflecting open/closed.
    Static hint
    The trigger carries aria-expanded bound to the listbox's visibility.
    Runtime check
    Open and close the listbox; assert aria-expanded matches its rendered visibility every time.
  • StaticRuntimeThe button carries `aria-controls="IDREF"` pointing to the listbox element.
    Static hint
    The trigger carries aria-controls whose value matches the listbox id.
    Runtime check
    Assert the aria-controls IDREF resolves to the rendered listbox.
  • StaticRuntimeThe button must have an accessible name that includes the field label and current value.
    Static hint
    The trigger carries aria-labelledby referencing both the label element and the element holding the current value.
    Runtime check
    Compute the trigger's accessible name; assert it contains both the field label text and the currently selected value.
  • StaticThe popup contains one element with `role="listbox"` and a stable ID (the same one referenced by `aria-controls`).
    Static hint
    Exactly one element in the popup carries role="listbox", with an id that does not change across renders.
  • StaticEach option uses `role="option"` and has a stable ID.
    Static hint
    Each option carries role="option" and a deterministic id.
  • StaticRuntimeEach option exposes selection with `aria-selected="true|false"`.
    Static hint
    Each option carries aria-selected bound to selection state.
    Runtime check
    Select an option; assert aria-selected is true on it and false on the others.
  • StaticRuntimeWhen the listbox is open, the "currently active" option is programmatically indicated (recommended).
    Static hint
    Focus stays on the button and aria-activedescendant points at the active option, or an equivalent documented mechanism is used consistently.
    Runtime check
    Arrow through the open listbox; assert the active option is programmatically identified and tracks the highlight.
  • StaticRuntimeThe listbox must be shown/hidden in the DOM so that when closed it cannot be reached by keyboard or screen readers (e.g., via `hidden`).
    Static hint
    The closed listbox uses the hidden attribute or is unmounted; flag CSS-only hiding.
    Runtime check
    With the listbox closed, assert its options are absent from the accessibility tree.
  • RuntimeTab / Shift+Tab moves focus to/from the button like a normal form field (no focus trap).
    Runtime check
    Tab into and out of the field; assert focus enters and leaves the button without being trapped.
  • RuntimeEnter or Space on the button opens the listbox.
    Runtime check
    Focus the button and press Enter, then Space; assert each opens the listbox.
  • RuntimeArrow Up/Down moves the active option.
    Runtime check
    With the listbox open, press Arrow Up and Arrow Down; assert the active option moves.
  • RuntimeEnter or Space selects the active option and closes the listbox.
    Runtime check
    With an option active, press Enter, then repeat with Space; assert each selects it and closes the listbox.
  • RuntimeEsc closes the listbox and returns the user to the button state.
    Runtime check
    With the listbox open, press Esc; assert it closes and focus is on the button.
  • RuntimeClicking/tapping outside closes the listbox.
    Runtime check
    With the listbox open, click outside the control; assert it closes.

Don'ts

  • StaticDo not use `role="menu"` / `role="menuitem"` for select options.
    Static hint
    Flag role="menu" or role="menuitem" on the popup or its options.
  • StaticRuntimeDo not make each option tabbable (avoid forcing users to Tab through options).
    Static hint
    Flag tabindex="0" on option elements.
    Runtime check
    With the listbox open, press Tab; assert focus does not step through the options.
select.native6 rules6 Static1 Runtime

Must Haves

  • StaticUse a native `<select>` element with native `<option>` elements.
    Static hint
    Control is a native <select> whose children are <option> (or <optgroup>) elements.
  • StaticRuntimeProvide a visible label associated with the `<select>` via a native `<label>` using `for`/`id`.
    Static hint
    A <label for> / htmlFor points at the select's id, or the select is wrapped in a <label>.
    Runtime check
    Assert the label renders visible and resolves into the select's computed accessible name.
  • StaticThe `<select>` participates in the form via a `name` attribute, and uses native attributes (`required`, `disabled`) as needed.
    Static hint
    The select carries a name attribute; required and disabled use the native attributes rather than aria-required or aria-disabled.
  • StaticKeep option content as plain text.
    Static hint
    <option> elements contain text only — no elements, images, or markup.
  • StaticStyle the control with CSS only (`appearance` and related properties).
    Static hint
    Styling is applied through CSS on the native select; no scripted re-implementation of the control's appearance or behavior.

Don'ts

  • StaticDo not use the `multiple` attribute (native multi-select tests very poorly).
    Static hint
    Flag the multiple attribute on the select.
spinner.basic21 rules10 Static6 LLM eval10 Runtime

Must Haves

  • StaticThe graphic that draws the spinner has `aria-hidden="true"`, no `role`, no `aria-label`, and no text content.
    Static hint
    The spinner graphic carries aria-hidden="true" and no role, aria-label, or text content.
  • StaticThe loading state is carried by the host, meaning the control or region the spinner sits inside, together with a live region holding the text that describes the wait.
    Static hint
    The busy state is expressed on the host element plus a live region; the graphic itself carries no state.
  • StaticRuntimeWhen a region is fetching or replacing its content, that region has `aria-busy="true"` for the duration, and the attribute is removed or set to `"false"` once the content arrives.
    Static hint
    The fetching region carries aria-busy bound to the in-flight state.
    Runtime check
    Start and finish a fetch; assert aria-busy is "true" during and absent or "false" after.
  • StaticRuntimeThe spinner is not focusable and is not in the page tab sequence.
    Static hint
    The spinner carries no tabindex and is not a natively focusable element.
    Runtime check
    Tab through the page while the spinner shows; assert focus never lands on it.
  • RuntimeThe spinner contributes nothing to the accessible name of its host. Adding the graphic does not change what the host control or region is called.
    Runtime check
    Compute the host's accessible name with and without the spinner rendered; assert it is identical.
  • StaticLLM evalThe host names what is loading: a control through its own label, and a region through its heading referenced by `aria-labelledby`, or through `aria-label`.
    Static hint
    The host has a name source: its own label, aria-labelledby pointing at a heading, or aria-label.
    LLM eval question
    Does the host's name identify what is loading, rather than leaving the wait unattributed?
  • RuntimeWhen the host's visible label changes while busy (e.g., "Save" becomes "Saving"), the changed text is the host's accessible name.
    Runtime check
    Put the control in its busy state; assert its computed accessible name is the changed visible text.
  • StaticRuntimeA live region is present in the DOM before the spinner appears, and receives text describing the wait. Use `role="status"`, or an equivalent such as `aria-live="polite"` and `aria-atomic="true"`.
    Static hint
    The live region is rendered unconditionally and carries role="status", or aria-live="polite" with aria-atomic="true".
    Runtime check
    Before the spinner appears, assert the live region already exists in the DOM.
  • LLM evalThe text begins with a status word: "Loading" for a region fetching content, or the progressive form of a control's own label for a control ("Save" becomes "Saving").
    LLM eval question
    Does the announcement begin with a status word — "Loading" for a region, or the progressive form of the control's own label?
  • LLM evalWhen the button or region has a short accessible name, append it, taking the words from that existing name rather than authoring new ones (e.g., a panel named "Recommendations" gives "Loading recommendations"). "Loading" on its own is correct when there is no short name to borrow.
    LLM eval question
    Does the announcement borrow the host's existing name rather than introducing new wording, and fall back to "Loading" alone when there is no short name?
  • RuntimeThe live region receives a message when the wait ends, on success and on failure alike.
    Runtime check
    Complete a wait and separately fail one; assert the live region receives an end message in both cases.
  • LLM evalThe end message reuses the words of the start message in completed form (e.g., "Loading recommendations" becomes "Recommendations loaded").
    LLM eval question
    Does the end message reuse the start message's words in completed form, rather than introducing different wording?
  • RuntimeThe live region text is removed once it is no longer current, so a stale message is not discoverable later by a user browsing the page.
    Runtime check
    After the wait ends and the message has been announced, assert the live region is cleared.
  • StaticRuntimeA control that becomes busy after activation uses `aria-disabled="true"` rather than the native `disabled` attribute, so the control keeps keyboard focus for the duration of the wait.
    Static hint
    Busy controls carry aria-disabled; flag the native disabled attribute applied on activation.
    Runtime check
    Activate the control by keyboard; assert it retains focus for the duration of the wait.
  • StaticThe activation handler returns early while the control is busy, since `aria-disabled` does not block activation on its own.
    Static hint
    The activation handler guards on the busy state and returns before performing its action.
  • RuntimeUnder `prefers-reduced-motion: reduce`, the animated graphic is not shown, and visible text stating what is happening is rendered in its place.
    Runtime check
    Emulate prefers-reduced-motion: reduce; assert no animated graphic renders and visible status text appears instead.
  • RuntimeThe spinner is removed when the wait ends, and a failed wait is replaced by an error message rather than a graphic that spins indefinitely.
    Runtime check
    Fail a request; assert the spinner is removed and an error message is rendered.

Don'ts

  • StaticDo not put `role="progressbar"` on a spinner that has no value, which reports a range widget with nothing to report.
    Static hint
    Flag role="progressbar" on a spinner carrying no aria-valuenow.
  • StaticDo not put `aria-live` on the spinner graphic or on the element that mounts and unmounts with it, since a live region that is not in the DOM before the change does not announce reliably.
    Static hint
    Flag aria-live on the spinner graphic or on any element rendered conditionally alongside it.
  • LLM evalDo not place several spinners in one view for content arriving in pieces.
    LLM eval question
    Would a single busy indicator for the view serve better than one spinner per arriving piece?
  • LLM evalDo not attach a spinner to every asynchronous call. Add one when the wait is long enough for the user to notice it, or when the design asks for a busy state on a specific control.
    LLM eval question
    Is this wait long enough for the user to perceive, or does the design call for a busy state here?
switch.basic15 rules8 Static4 LLM eval7 Runtime

Must Haves

  • StaticThe switch has `role="switch"`.
    Static hint
    Control carries role="switch" (or is an input[type="checkbox"] with role="switch").
  • StaticRuntimeThe switch has a visible text label, either programmatically associated with the control (`label[for]`) or matched by an accessible name on the control (`aria-label` or `aria-labelledby`).
    Static hint
    A labelling mechanism is authored: label[for], aria-labelledby, aria-label, or a wrapping label.
    Runtime check
    Render the switch; assert the label text is visible and its computed accessible name is non-empty and derived from that label.
  • RuntimeThe accessible name begins with the visible label text, and may append a short amount of additional context for screen reader users (via `aria-label`, `aria-labelledby`, or offscreen `.sr-only` text).
    Runtime check
    Compute the switch's accessible name and assert it begins with the visible label text.
  • LLM evalThe accessible name describes the setting the switch controls, worded so it is true when the switch is on (e.g., "Enable notifications").
    LLM eval question
    Does the accessible name name the setting being controlled, phrased so it reads true when the switch is on, rather than being vague or stateful ("On", "Toggle")?
  • StaticThe switch's on/off state is exposed via `aria-checked` (`"true"`/`"false"`), or via the native `checked` property when the switch is an `input[type="checkbox"]`.
    Static hint
    Exactly one state mechanism is authored: aria-checked bound to state, or the native checked property on an input[type="checkbox"].
  • RuntimeThe exposed state stays in sync with the switch as it toggles.
    Runtime check
    Toggle the switch repeatedly; assert aria-checked (or native checked) matches the rendered on/off state after every toggle.
  • StaticRuntimeThe switch is focusable so Tab and Shift+Tab reach it: a native `input[type="checkbox"]` is focusable by default; a non-native element (`div`/`button` with `role="switch"`) must include `tabIndex="0"`.
    Static hint
    Switch is a natively focusable element, or a non-native element carrying tabindex="0".
    Runtime check
    Tab and Shift+Tab through the page; assert the switch receives focus from both directions (catches an inert or hidden ancestor that markup alone cannot reveal).
  • RuntimeSpace toggles the switch.
    Runtime check
    Focus the switch and press Space; assert the state toggles and the page does not scroll.
  • RuntimeEnter toggles the switch.
    Runtime check
    Focus the switch and press Enter; assert the state toggles. Exception: for a native input[type="checkbox"] implementation, Enter is not expected to toggle — assert Space alone does.
  • StaticLLM evalIf multiple switches are presented as a labeled set, group them with `fieldset` + `legend` or `role="group"` + `aria-labelledby`.
    Static hint
    A set of sibling switches is wrapped in fieldset+legend or role="group"+aria-labelledby.
    LLM eval question
    Do these switches form a labeled set that a user would read as one group, and if so, is the group structure present?
  • LLM evalThe group's label describes the purpose of the set (WCAG 2.4.6).
    LLM eval question
    Does the legend or group label describe what the set of switches is for, rather than being generic or a restatement of one member?
  • StaticRuntimeAssociate any additional descriptive static text with the switch via `aria-describedby` on the switch element itself.
    Static hint
    Descriptive text is referenced by aria-describedby on the switch element, with a matching id authored.
    Runtime check
    Assert the aria-describedby IDREF resolves to a rendered element and contributes the expected description.
  • StaticDo not place `aria-describedby` on a `fieldset` or group container.
    Static hint
    Flag aria-describedby authored on a fieldset or [role="group"] container rather than on the switch.

Don'ts

  • LLM evalDo not use a switch for non-setting actions; use it only for persistent on/off settings.
    LLM eval question
    Does this control persist an on/off setting, or does it fire a one-shot action or navigation (which should be a button)?
  • StaticDo not use both `checked` and `aria-checked` on `input[type="checkbox"]`.
    Static hint
    Flag a checkbox input that carries both the native checked property and aria-checked.
tabs.basic30 rules16 Static4 LLM eval20 Runtime

Must Haves

  • StaticThe set of tabs is contained in an element with `role="tablist"`.
    Static hint
    A container element carries role="tablist".
  • StaticEach tab has `role="tab"` and is contained within the tablist.
    Static hint
    Every element with role="tab" is a descendant of the role="tablist" container.
  • StaticEach tab uses a native `<button>` as its base element (preferred), or a non-native element with `role="tab"` only when a native button cannot be used.
    Static hint
    Each role="tab" element is a <button>, or a non-native element carrying role="tab".
  • StaticRuntimeIf a non-native element is used, add `tabindex` and keyboard support for Enter and Space, ensuring Space prevents page scrolling while activating the tab.
    Static hint
    Non-native tab elements carry a tabindex attribute and a keydown handler covering Enter and Space with preventDefault on Space.
    Runtime check
    Focus a non-native tab and press Space; assert the tab activates and the page scroll position is unchanged.
  • StaticEach panel is an element with `role="tabpanel"`.
    Static hint
    One element with role="tabpanel" exists per tab.
  • RuntimeExactly one tabpanel is displayed at a time.
    Runtime check
    Activate each tab in turn; after each activation assert exactly one role="tabpanel" element is visible.
  • StaticRuntimeEach tabpanel is shown/hidden in the DOM (e.g., via the `hidden` attribute), so that when hidden, its content cannot be reached by keyboard or screen readers.
    Static hint
    Inactive tabpanels carry the hidden attribute, display: none, or are not rendered; they are not merely positioned offscreen.
    Runtime check
    With one tab selected, assert every other tabpanel is absent from the accessibility tree and that no element inside it can receive focus via Tab.
  • StaticLLM evalA vertically stacked tablist has `aria-orientation="vertical"`; a horizontal tablist omits the attribute.
    Static hint
    aria-orientation="vertical" is present when the tablist is laid out vertically and absent when it is laid out horizontally.
    LLM eval question
    When the layout cannot be resolved from the markup alone, do the applied styles render the tablist vertically, and does the attribute match?
  • StaticRuntimeThe tablist has an accessible name, via `aria-labelledby` referencing a visible heading when one exists, otherwise via `aria-label`.
    Static hint
    The role="tablist" element carries aria-labelledby or aria-label; any aria-labelledby target id exists in the document.
    Runtime check
    Compute the tablist's accessible name; assert it is non-empty.
  • StaticRuntimeEach tab's visible text is contained in its accessible name, and appears at the start of it when additional context is appended.
    Static hint
    Any aria-label on a tab begins with that tab's visible text rather than replacing it.
    Runtime check
    Compute each tab's accessible name; assert it starts with the tab's visible text (WCAG 2.5.3 Label in Name).
  • StaticEach tabpanel has `aria-labelledby` referencing the `id` of its tab.
    Static hint
    Every role="tabpanel" carries aria-labelledby whose value is the id of its corresponding role="tab" element.
  • StaticThe selected tab has `aria-selected="true"` and every other tab has `aria-selected="false"`.
    Static hint
    aria-selected is bound on every role="tab" element to an expression that resolves to "true" for exactly one tab and "false" for the rest.
  • StaticThe `"false"` value is set explicitly, including on a tab that holds focus without being selected under manual activation.
    Static hint
    No branch renders a role="tab" element without an aria-selected attribute.
  • StaticEach tab has `aria-controls` referencing the `id` of its tabpanel.
    Static hint
    Every role="tab" carries aria-controls whose value is the id of its role="tabpanel".
  • RuntimeThe selected tab is distinguishable from unselected tabs by more than color (e.g., weight, an underline, or an icon), per `global.use-of-color`.
    Runtime check
    Compare computed styles of the selected and an unselected tab; assert at least one non-color property differs (font-weight, border, text-decoration, or an added child element).
  • RuntimeLLM evalThe tab set implements one activation model consistently (see Customizable for choosing one).
    LLM eval question
    When runtime cannot exercise the component, do the arrow-key path and the click path resolve to the same activation model?
    Runtime check
    Arrow to an adjacent tab and record whether selection changed; click a third tab and record the same. Assert both paths agree on whether focus alone selects.
  • RuntimeUnder automatic activation, arrow-key movement also selects the newly focused tab and displays its panel.
    Runtime check
    Under automatic activation, press Right Arrow; assert the newly focused tab has aria-selected="true" and its panel is the visible one.
  • RuntimeUnder manual activation, arrow-key movement changes focus only, and the selected tab and displayed panel do not change until Enter or Space.
    Runtime check
    Under manual activation, arrow to an unselected tab and assert aria-selected and the visible panel are unchanged; press Enter and assert both update.
  • RuntimeLLM evalRight Arrow and Left Arrow move focus between tabs and wrap at the ends.
    LLM eval question
    When runtime cannot exercise the component, do the ArrowRight and ArrowLeft branches move focus one tab in each direction and wrap at both ends?
    Runtime check
    Focus the first tab; press Left Arrow and assert focus is on the last tab; press Right Arrow and assert focus returns to the first.
  • RuntimeIn a vertical tablist, Down Arrow performs as Right Arrow and Up Arrow performs as Left Arrow.
    Runtime check
    In a vertical tablist, press Down Arrow and Up Arrow; assert focus moves to the next and previous tab respectively.
  • RuntimeA horizontal tablist leaves Up and Down Arrow alone so the page can still scroll.
    Runtime check
    In a horizontal tablist, focus a tab and press Down Arrow; assert focus does not move and the page scroll position changes.
  • RuntimeHome moves focus to the first tab and End moves focus to the last tab.
    Runtime check
    From a middle tab, press Home and assert focus is on the first tab; press End and assert focus is on the last tab.
  • RuntimeEnter or Space activates the focused tab when it was not already activated on focus.
    Runtime check
    Under manual activation, arrow to an unselected tab and press Enter; assert its panel becomes visible. Repeat with Space.
  • StaticRuntimeTab moves focus out of the tablist to the next element in the page tab sequence, never to another tab.
    Static hint
    Exactly one tab is bound to tabindex="0"; all others are bound to tabindex="-1".
    Runtime check
    Focus a tab and press Tab; assert focus leaves the tablist and does not land on another role="tab" element.
  • StaticThe selected tab has `tabindex="0"` and every other tab has `tabindex="-1"`, so the tablist is a single Tab stop.
    Static hint
    tabindex on each tab is bound to the selection state, resolving to 0 for the selected tab and -1 for the rest.
  • StaticRuntimeWhen the tabpanel's content begins with a non-focusable element, the tabpanel has `tabindex="0"`, so that Tab from the tablist reaches the panel content instead of skipping past it.
    Static hint
    Tabpanels whose first content element is not natively focusable carry tabindex="0".
    Runtime check
    With a panel whose content starts non-focusable, focus the selected tab and press Tab; assert focus lands on the tabpanel element.
  • RuntimeThe focus indicator on a focused tab is distinguishable from the selected-tab styling.
    Runtime check
    Under manual activation, arrow to an unselected tab; assert its computed focus styling differs from the selected tab's styling.

Don'ts

  • RuntimeDo not leave a tabpanel visible while its tab has `aria-selected="false"` (and vice versa).
    Runtime check
    After each tab activation, assert the visible tabpanel is the one referenced by the tab whose aria-selected is "true", and no other panel is visible.
  • LLM evalRuntimeDo not derive arrow-key movement from the selected tab's index instead of the focused tab's index.
    LLM eval question
    Does the arrow-key handler compute the next index from the index of the tab that currently has focus, rather than from the selected-tab state variable?
    Runtime check
    Under manual activation, arrow twice in one direction from the selected tab; assert focus advanced two tabs rather than returning to the first neighbor.
  • RuntimeDo not move focus off the activated tab.
    Runtime check
    Activate each tab by click and by Enter; assert document.activeElement remains that role="tab" element and the document URL is unchanged.
toast.basic9 rules4 Static6 Runtime

Must Haves

  • StaticRuntimeThe live region container must be present in the DOM when the page/view loads.
    Static hint
    The live region container is rendered unconditionally; flag a container behind a conditional render tied to toast visibility.
    Runtime check
    With no toast showing, assert the live region container exists in the DOM and is empty.
  • StaticThe toast message must be announced via `role="status"`, or an equivalent such as `aria-live="polite"` and `aria-atomic="true"`.
    Static hint
    The live region carries role="status", or aria-live="polite" together with aria-atomic="true".
  • RuntimeWhen a toast is triggered, its message text must be injected into the existing live region container.
    Runtime check
    Trigger a toast; assert the message text appears inside the pre-existing live region element rather than a newly mounted one.
  • RuntimeThe toast must not move focus automatically when it appears.
    Runtime check
    Trigger a toast from a control; assert document.activeElement is unchanged.
  • RuntimeThe toast must disappear automatically (recommended ~5 seconds).
    Runtime check
    Trigger a toast and wait; assert it is removed without user interaction.
  • RuntimeThe live region text must be cleared when the toast dismisses to avoid stale messages being discovered later.
    Runtime check
    Let a toast auto-dismiss; assert the live region is empty afterward.
  • RuntimeIf a dismiss button is present, it must not steal focus when the toast appears.
    Runtime check
    Trigger a toast that renders a dismiss button; assert focus stays on the triggering control.

Don'ts

  • StaticDo not use `role="alertdialog"`.
    Static hint
    Flag role="alertdialog" on the toast or its container.
  • StaticThe toast must not contain buttons or elements that require user action (use `snackbar` or `dialog` instead).
    Static hint
    Flag interactive controls inside the toast other than a single dismiss button.
tooltip.basic13 rules8 Static8 Runtime

Must Haves

  • StaticThe tooltip bubble is an element with `role="tooltip"` and a stable `id`.
    Static hint
    The tooltip element carries role="tooltip" and an id that does not change across renders.
  • StaticRuntimeThe trigger references the tooltip with `aria-describedby` pointing to the tooltip's `id`.
    Static hint
    The trigger carries aria-describedby whose value matches the tooltip's id.
    Runtime check
    Assert the aria-describedby IDREF resolves to the rendered tooltip.
  • StaticRuntimeThe tooltip supplements the accessible name, it does not replace it. The trigger has its own accessible name (e.g., visible text or `aria-label`).
    Static hint
    The trigger has its own name source; flag aria-labelledby pointing at the tooltip.
    Runtime check
    Compute the trigger's accessible name; assert it is non-empty and does not come from the tooltip text.
  • RuntimeThe tooltip appears on both pointer hover and keyboard focus of the trigger.
    Runtime check
    Hover the trigger and assert the tooltip appears; separately focus it by keyboard and assert it appears.
  • RuntimeEsc dismisses the tooltip whether it was opened by pointer hover or by keyboard focus, and does not move pointer hover or keyboard focus (dismissible).
    Runtime check
    Open the tooltip by hover and press Esc; repeat by keyboard focus. Assert it hides both times and focus is unchanged.
  • StaticBind the Esc handler at the document level, not only on the trigger, so it works when the tooltip is shown by hover and the trigger does not hold focus.
    Static hint
    The Esc handler is registered on document, not only as an onKeyDown on the trigger.
  • RuntimeWhen the trigger has keyboard focus, the trigger keeps focus after dismissal.
    Runtime check
    Focus the trigger, press Esc; assert document.activeElement is still the trigger.
  • RuntimeThe tooltip remains visible while the pointer is over the trigger or over the tooltip, and while the trigger has focus (hoverable and persistent).
    Runtime check
    Move the pointer from the trigger onto the tooltip; assert it stays visible. Assert it also stays while the trigger holds keyboard focus.
  • StaticTrack hover and focus as independent conditions and keep the tooltip open while either is active, so a stray pointer movement does not hide it while the trigger still has keyboard focus.
    Static hint
    Hover and focus are two separate state values combined with a logical OR; flag a single boolean shared by both.
  • StaticRuntimeThe tooltip is not focusable and contains plain text only, with no interactive content.
    Static hint
    Flag interactive elements or a tabindex inside the tooltip.
    Runtime check
    Tab through the page with the tooltip shown; assert focus never lands inside it.

Don'ts

  • StaticDo not rely on the native `title` attribute as the tooltip; it is not keyboard-accessible and is announced inconsistently.
    Static hint
    Flag a title attribute used as the tooltip mechanism on the trigger.
  • StaticDo not use `role="menu"` on the tooltip.
    Static hint
    Flag role="menu" on the tooltip element.
  • RuntimeDo not leave the tooltip visible after the trigger has lost focus and the pointer has left both the trigger and the tooltip.
    Runtime check
    Blur the trigger and move the pointer off both trigger and tooltip; assert the tooltip hides.