/SKILL.md
Build accessible web user interfaces that meet WCAG 2.2 Level AA. Use whenever generating HTML, CSS, JSX, TSX, React, Vue, or Svelte components, pages, forms, modals, or email templates - including small snippets.
npx skillsauth add intopia/intopia-web-accessibility-skill intopia-web-accessibilityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply these accessibility principles whenever generating or modifying UI code. The goal is interfaces usable by people of all abilities, without workarounds.
references/colour-contrast/Colour Contrast Reference.md.Per-component references live in references/. Use this index to locate the relevant files before generating code. A dash (—) means no file exists for that component in that category; fall back to the Core Accessibility Principles.
| Component | Acceptance Criteria | Code Example |
|---|---|---|
| Accordion | references/acceptance-criteria/Acceptance Criteria - Accordion.md | — |
| Button | references/acceptance-criteria/Acceptance Criteria - Button.md | references/code-example/Code example - Button.md |
| Checkbox | references/acceptance-criteria/Acceptance Criteria - Checkbox.md | — |
| Checkbox Group | references/acceptance-criteria/Acceptance Criteria - Checkbox Group.md | — |
| Complex Image (diagram, graph, infographic) | references/acceptance-criteria/Acceptance Criteria - Complex Image (e.g. diagram, graph, infographic).md | references/code-example/Code example - Complex Image (e.g. diagram, graph, infographic).md |
| Heading | references/acceptance-criteria/Acceptance Criteria - Heading.md | references/code-example/Code example - Heading.md |
| Image | references/acceptance-criteria/Acceptance Criteria - Image.md | references/code-example/Code example - Image.md |
| Landmark | references/acceptance-criteria/Acceptance Criteria - Landmark.md | references/code-example/Code example - Landmark.md |
| Link | references/acceptance-criteria/Acceptance Criteria - Link.md | references/code-example/Code example - Link.md |
| List | references/acceptance-criteria/Acceptance Criteria - List.md | references/code-example/Code example - List.md |
| Modal Dialog | references/acceptance-criteria/Acceptance Criteria - Modal Dialog.md | — |
| Page Language | references/acceptance-criteria/Acceptance Criteria - Page Language.md | references/code-example/Code example - Page language.md |
| Page Title | references/acceptance-criteria/Acceptance Criteria - Page Title.md | references/code-example/Code example - Page Title.md |
| Radio Group | references/acceptance-criteria/Acceptance Criteria - Radio Group.md | references/code-example/Code example - Radio Group.md |
| Select | references/acceptance-criteria/Acceptance Criteria - Select.md | — |
| Table | references/acceptance-criteria/Acceptance Criteria - Table.md | references/code-example/Code example - Table.md |
| Tabs | references/acceptance-criteria/Acceptance Criteria - Tabs.md | — |
| Text Field | references/acceptance-criteria/Acceptance Criteria - Text Field.md | references/code-example/Code example - Text Field.md |
| Toggletip | references/acceptance-criteria/Acceptance Criteria - Toggletip.md | — |
| Tooltip | references/acceptance-criteria/Acceptance Criteria - Tooltip.md | — |
Cross-cutting references
references/colour-contrast/Colour Contrast Reference.md<title>: descriptive and unique per page.lang attribute: set on <html> and matches the content language.<header>, <nav>, <main>, <footer>, <aside> for structure.<h1> through <h6> in hierarchical order. Do not skip levels. Don't visually hide headings unless instructed to do so.Use the correct list type:
<ul>: related items where order does not matter (navigation links, feature lists, search results).<ol>: items where sequence matters (steps, rankings, instructions).<dl>: term/value pairs (glossaries, metadata, key-value data).Do not use list markup for visual indentation. Avoid suppressing list semantics with list-style: none; some screen readers remove list semantics when this CSS is applied.
Use <table> for data with meaningful row/column relationships. Never use tables for visual layout.
<th> for header cells, always with a scope attribute (scope="col" or scope="row").<caption> to name the table. Prefer <caption> over aria-label on the table element.<thead>, <tbody>, and <tfoot> to group rows semantically.id and headers to associate cells with their headers explicitly.role="presentation" to suppress table semantics. Do not use <th> in layout tables.<button> inside the <th> and set aria-sort (ascending, descending, or none) on that <th>. Only one column is sorted at a time; the rest are none.<th scope="col" aria-sort="ascending"><button type="button">Name</button></th>
<th scope="col" aria-sort="none"><button type="button">Role</button></th>
<button>: triggers an action (submit, open modal, toggle, expand). Activated by Enter and Space.<a href>: navigates to a location (page, anchor, URL). Activated by Enter.Never use <div>, <span>, or other non-interactive elements as buttons or links. If the design requires custom styling, style a native element rather than using ARIA to patch an incorrect one. If a link performs an action rather than navigating, use <button>.
Every interactive element must have an accessible name. Apply in this order of preference:
<label>, <caption>, <figcaption>, button text, link text.aria-labelledby: references visible text already on the page by id. Preferred when visible text exists.aria-label: use only when no visible text is available (e.g. an icon-only button). The value must match or begin with any visible text on the element (label-in-name requirement).Do not use aria-label to override or contradict visible text.
<!-- Icon-only button: use aria-label --> <button type="button" aria-label="Close dialog"> <svg aria-hidden="true" focusable="false">...</svg> </button> <!-- Label via aria-labelledby (preferred when visible text exists) --> <h2 id="billing-heading">Billing address</h2> <form aria-labelledby="billing-heading">...</form>
Prefer native HTML elements over ARIA roles. When a custom component is unavoidable:
role="dialog", role="tab", role="combobox").role="button" to <button>).role="option" must be inside role="listbox").Interactive elements must expose their current state programmatically.
| Pattern | Attribute |
| :----------------------------------------- | :-------------------------------------------- |
| Expandable control (accordion, disclosure) | aria-expanded="true" / "false" |
| Toggle button | aria-pressed="true" / "false" |
| Tab / option / treeitem | aria-selected="true" / "false" |
| Checkbox or switch | aria-checked="true" / "false" / "mixed" |
| Functionally disabled (still focusable) | aria-disabled="true" |
| Current page in navigation | aria-current="page" |
Rule (prose):
aria-pressed requires a true toggle. Use aria-pressed only when pressing the button switches it between an active and inactive state, and the same button press deactivates it. If the button is one of a group where activation is exclusive (pressing it turns it on, but another button turns it off), it is not a toggle. In that case, either use a role="radio" group or communicate the active item with aria-current="true". Using aria-pressed on a button that cannot be deactivated by its own press misrepresents the interaction to assistive technology.
Use aria-disabled="true" instead of the disabled attribute when the element should remain focusable, so keyboard users can discover it and understand why it is unavailable. Pair it with a visible explanation where possible.
aria-hidden="true": removes an element and its children from the accessibility tree. Use for decorative icons, duplicate text, and visually redundant content. Never apply to focusable elements.hidden or display: none: removes content from both visual rendering and the accessibility tree. Use when content is not present..visually-hidden / .sr-only CSS class that clips the element without using display: none or visibility: hidden..visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
tabindex="0" to include custom elements in the tab order.tabindex="-1" only for programmatic focus management.tabindex values greater than 0.outline: 2px solid with outline-offset: 2px for focus states. Do not rely on the default browser focus ring in environments that apply outline: 0 or outline: none.:focus-visible to show focus rings for keyboard users without affecting mouse users.outline without an equally visible replacement.| Pattern | Required behaviour |
| :---------------- | :----------------------------------------------------------- |
| Modal opens | Move focus to modal heading. Trap focus inside. Return focus to trigger on close. |
| Menu opens | Move focus to first item. Return focus to menu button on close. |
| Toast / alert | Use role="status" or role="alert" with aria-live. Do not move focus. |
| Accordion expands | Focus stays on the header trigger. Do not move it. |
| SPA navigation | Move focus to new page heading or <main>. |
Use aria-live only when all three conditions are true:
role="status" (aria-live="polite"): non-urgent updates such as search result counts and filter feedback.role="alert" (aria-live="assertive"): critical, time-sensitive errors only.aria-atomic="true" only when the whole region should be read as a unit.A static progress bar does not need ARIA roles — its information is best conveyed as text (e.g. "Step 2 of 4" or "60% complete"). If that text is not part of the visual design, include it visually hidden so screen reader users still get it.
<div class="progress-track"><div class="progress-fill" style="width: 60%"></div></div>
<span class="visually-hidden">60% complete</span>
If the progress bar updates dynamically, use the progressbar role with value attributes so assistive technology announces changes:
<div role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress"></div>
<label> for every input. Associate explicitly with for/id; do not rely on wrapping alone.<fieldset> and <legend> for grouped inputs (radio groups, checkboxes, date field sets, address field sets).placeholder as a substitute for a <label>. Placeholder text disappears on input, has insufficient contrast by default, and is not reliably announced by screen readers. Exception: A Search icon is a valid visual label for a search field providing the field has an accessible name e.g. Search.type attribute: email, tel, number, date, password, search, url. This provides appropriate keyboards on mobile and semantic meaning.autocomplete attributes for personal data fields (e.g. autocomplete="given-name", autocomplete="email", autocomplete="current-password"). Required by WCAG 1.3.5.required and aria-required="true" on the input.<label for="name">Full name <span aria-hidden="true">*</span></label> <input type="text" id="name" name="name" required aria-required="true" autocomplete="name">
aria-describedby to associate hints, constraints, or instructions with an input.required, type="email" default popups). Always provide custom inline error messages.aria-invalid="true" on inputs with validation errors.aria-describedby to associate the error message with the input.role="alert" or aria-live="assertive" on error containers when focus is already being moved to the error.<label for="email">Email address</label> <input type="email" id="email" name="email" required aria-required="true" aria-invalid="true" aria-describedby="email-hint email-error" > <p id="email-hint">We'll never share your email.</p> <p id="email-error">Enter a valid email address.</p>
disabled: removes the element from the tab order and prevents interaction. Use only when the field genuinely cannot be used.readonly: keeps the field focusable and its value submittable. Use when the value is visible but should not be changed.disabled and required.Any drag-and-drop interaction (reorderable lists, kanban boards, file sorting, repositioning items) must be fully operable without a pointer drag gesture. Pointer dragging may remain as an enhancement, but it must never be the only way to perform the action.
Provide a keyboard-operable mechanism using one of the following patterns. Either pattern must use native <button> elements, follow a logical tab order, and have visible focus indicators meeting 3:1 contrast.
<button>. On activation (Enter or Space) the item enters a "grabbed" state where arrow keys move it between valid positions, Enter or Space drops it, and Escape cancels and returns it to its original position.<button> (e.g. "Move item") that opens a menu or exposes controls letting the user choose a new position directly (move up, move down, move to top, move to a named target). This is often simpler and more robust than a grabbed state, particularly when moving items between containers.The interaction must expose its state and outcome to screen reader users:
aria-describedby (e.g. "Press Enter to pick up, arrow keys to move, Escape to cancel").role="status" / aria-live="polite"): pick-up ("Grabbed Task A, position 2 of 5"), each move ("Task A, now position 3 of 5"), drop ("Dropped Task A at position 3 of 5"), and cancel ("Cancelled, Task A returned to position 2 of 5"). Do not use aria-live="assertive" for reorder status; assertive is reserved for critical, time-sensitive content.aria-pressed to convey the grabbed state; communicate it through the live region announcement and aria-describedby help text instead.| Content type | Minimum ratio | | :---------------------------------------------- | :------------ | | Normal text (below 24px regular / 18.66px bold) | 4.5:1 | | Large text (24px+ regular / 18.66px+ bold) | 3:1 | | UI components (borders, focus indicators) | 3:1 | | Meaningful icons and graphical objects | 3:1 |
Form field borders and focus states are frequent failure points. Always check both explicitly. Verify every pair using the Colour Contrast Workflow below.
| Image type | Requirement |
| :--------------------------------- | :----------------------------------------------------------- |
| Decorative | alt="" (empty string; do not omit the attribute) |
| Informative | Describe the information conveyed, not the image literally |
| Functional (inside link or button) | Describe the action or destination |
| Complex (chart, diagram) | Use aria-describedby pointing to a nearby detailed text description |
A chart is a complex image; the visual alone is never sufficient.
Unless an interactive chart is explicitly requested, mark the chart (<svg>/<canvas>/wrapper) aria-hidden="true" and provide the data as the accessible equivalent: a marked-up <table> (see Tables) for exact values, or a text summary stating the takeaway (e.g. "Revenue rose steadily from Q1 to Q4, peaking at $1.2M") when the trend matters more. Place it adjacent to the chart or in a <details> disclosure directly after. The table or summary must be readable on its own, not gated behind hover or mouse-only controls. Give the chart a visible heading or <figcaption> for sighted context; the accessible name lives on the table <caption> or summary.
The chart must still work visually: distinguish series without relying on colour alone (1.4.1) using labels, patterns, or markers; meet 3:1 for meaningful data elements (1.4.11) and text contrast (4.5:1, or 3:1 large) for labels, axes, and legends.
If an interactive chart is explicitly requested, the chart cannot be aria-hidden: each focusable point needs a visible focus indicator and an accessible name conveying its value, still provide the data table, and respect prefers-reduced-motion.
Content must reflow without loss of information or functionality at a viewport width of 320 CSS pixels (vertical scrolling) or a height of 256 CSS pixels (horizontal scrolling). Users must not be required to scroll in two dimensions to read or interact with content. Tables, complex data visualisations, maps, and toolbars are exempt where 2D layout is essential to usage or meaning.
<meta name="viewport" content="width=device-width, initial-scale=1"> in <head>.%, rem, em, vw, ch) for widths, padding, and font sizes. Avoid fixed pixel widths on layout containers.flex-wrap: wrap, or Grid with repeat(auto-fit, minmax(<min>, 1fr)), so columns collapse at narrow widths.overflow-wrap: anywhere or word-break: break-word. Long unbreakable strings are a common cause of 320px overflow.Generated code passes these checks before being presented to the user:
<title> and correct lang attributetabindex values greater than 0<fieldset>/<legend>aria-invalid and aria-describedby are applied on validation failurealt=""development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.