
Accessibility
- 4 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks during AI-assisted development.
About
accessibility is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- accessibility
- AI & Agent Building
- AI-coding skill
Accessibility by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,348 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill accessibilityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Accessibility
Semantic HTML is the foundation. ARIA is the patch. Keyboard access is non-negotiable. If an element looks interactive, it must be interactive for everyone.
Target WCAG 2.2 AA conformance unless the project explicitly specifies otherwise. AA includes all A criteria.
References
- Semantic HTML — `semantic-html.md`: Landmarks, headings,
images, tables, lists, language markup
- ARIA — `aria.md`: Roles, states, properties, naming, live regions,
common mistakes
- Keyboard — `keyboard.md`: Focus order, visibility, roving tabindex,
focus traps, restoration
- Forms — `forms.md`: Labels, grouping, required fields, validation,
autocomplete
- Component patterns — `component-patterns.md`: Dialog,
tabs, accordion, disclosure, menu, combobox, tooltip
- WCAG — `wcag.md`: Full WCAG 2.2 AA criteria tables and compliance
checklist
---
1. Semantic HTML
Use the right element for the right job. Native elements provide built-in keyboard support, screen reader announcements, and focus management that ARIA can only approximate.
Landmarks
- Every page must have exactly one
<main>. <header>/<footer>map tobanner/contentinfoonly as direct children of<body>-- nested inside<article>,
<section>, etc. they lose their landmark role.
- Label multiple
<nav>elements witharia-labeloraria-labelledby. - Never duplicate implicit roles (
<main role="main">,<nav role="navigation">). - Include all perceivable content within a landmark region.
For the full landmark-to-role mapping table, see `semantic-html.md`.
Headings
- One
<h1>per page identifying the primary content. - Never skip heading levels --
<h1>then<h3>breaks the outline. - Use headings for structure, not visual styling.
- Every
<section>and major content area should begin with a heading.
Interactive Elements
Use <button> for actions (submit, toggle, open dialog) -- activates via Enter and Space. Use <a href> for navigation -- activates via Enter. Never <a href="#" onclick="..."> for actions. Never <button> for navigation.
Tables
- Use
<th>withscope="col"orscope="row"for headers. - Add
<caption>to describe the table's purpose. - Never use tables for layout.
Lists
Use <ul> for unordered, <ol> for ordered, <dl>/<dt>/<dd> for term/description pairs. Screen readers announce list type and item count.
Images
- Every
<img>must have analtattribute -- even if empty. - Do not start alt text with "Image of" or "Picture of".
- Keep alt text under ~125 characters.
- For decorative images, prefer CSS
background-imageover<img alt="">. - Use real text, not images of text (WCAG 1.4.5).
For alt treatment by image type, see `semantic-html.md`.
Language and Page Metadata
- Set
<html lang="...">on every page (WCAG 3.1.1). - Mark inline language changes:
<span lang="fr">bonjour</span>(WCAG 3.1.2). - Expand abbreviations on first use with
<abbr>. - Provide a descriptive
<title>on every page (WCAG 2.4.2). - Provide a skip link as the first focusable element (WCAG 2.4.1).
---
2. ARIA
No ARIA is better than bad ARIA. ARIA modifies only the accessibility tree -- it does not change behavior, keyboard interaction, or appearance.
The Five Rules of ARIA
- Use native HTML first. If a semantic HTML element exists, use it.
- Do not change native semantics unless absolutely necessary. Never
<h2 role="tab">-- use
<div role="tab"><h2>...</h2></div>.
- All interactive ARIA controls must be keyboard operable. A
role="button"must respond to Enter and Space. - Never `role="presentation"` or `aria-hidden="true"` on focusable elements.
- All interactive elements must have an accessible name.
Naming and Describing
Priority: aria-labelledby > aria-label > <label> > element content
title.- Prefer visible labels over
aria-label. - Use
aria-labelledbyto compose names from multiple elements. aria-labelandaria-labelledbyreplace native label text, not supplement.- Never
aria-labelon elements withrole="presentation"orrole="none". - Never
aria-labelon<div>without a role -- ignored by most AT. - Use
aria-describedbyoraria-descriptionfor supplementary info after the name is established.
Live Regions
- Use
aria-live="polite"for non-urgent updates. - Use
aria-live="assertive"sparingly -- only errors, alerts, urgent info. - Set live regions in the DOM before content changes. Adding
aria-liveand content simultaneously may not be
announced.
- Inject alert content into an existing
role="alert"container for reliable announcement.
Place aria-expanded on the trigger element, not the panel.
For widget roles, state/relationship attribute tables, and common ARIA mistakes, see `aria.md`.
---
3. Keyboard Navigation
All interactive functionality must be operable with a keyboard alone.
Fundamental Keys
Tab/Shift+Tab between components. Arrow keys within composite widgets. Enter activates links, buttons, menu items. Space activates buttons, checkboxes, toggles. Escape closes overlays.
Focus Order
- Never
tabindex> 0. Rearrange DOM order instead. tabindex="0"makes non-interactive elements focusable (use sparingly).tabindex="-1"for programmatic focus only (dialog containers, skip targets).- Source order = visual order = focus order. CSS reordering (
flex-direction: row-reverse,order, grid) must not
break this.
Focus Visibility
- Never
outline: nonewithout a custom focus style replacement. - Use
:focus-visiblefor keyboard-only focus styles. - Focus indicator: at least 3:1 contrast against adjacent background.
- Focus indicator area: at least 2px border equivalent (WCAG 2.4.13).
- Focused element must not be entirely obscured (WCAG 2.4.11).
Focus Management
Use roving tabindex for composite widgets (tabs, toolbars, menus): active child gets tabindex="0", others get tabindex="-1". Use aria-activedescendant when the container must maintain focus (combobox).
Focus Trap (Dialogs)
1. On open: focus first focusable element inside dialog. 2. Trap Tab/Shift+Tab -- wrap last-to-first and first-to-last. 3. Escape closes dialog. 4. On close: return focus to the trigger element. 5. Set aria-modal="true".
Focus Restoration
- Deleted item -- focus next item, or previous if last was deleted.
- Closed overlay -- focus the trigger that opened it.
- Removed section -- focus nearest logical container or heading.
- Never let focus fall to
document.body.
Disabled Elements
- Remove disabled standalone controls from tab sequence (
disabledattribute ortabindex="-1"). - Keep disabled items focusable inside composite widgets (menus, tabs, trees, listboxes) so screen reader users can
discover them.
- Use
aria-disabled="true"to keep element focusable but not operable.
---
4. Accessible Forms
Labels
Every <input>, <select>, <textarea> must have a programmatic label.
Priority: <label for/id> > wrapping <label> > aria-labelledby > aria-label.
placeholderis not a label substitute -- disappears on input, unreliable in screen readers.- Visible label text must be contained in the accessible name (WCAG 2.5.3).
Grouping
- Group related controls with
<fieldset>+<legend>. Required for: radio groups, checkbox groups, related input sets
(address, date parts).
- Use
role="group"witharia-labelledbywhen<fieldset>is impractical.
Required Fields
- Use
requiredattribute oraria-required="true". - Indicate visually with asterisk + "\* Required" legend.
- Never color alone for required status.
Validation and Errors
- Display errors adjacent to the invalid field.
- Associate errors via
aria-describedbyoraria-errormessage. - Set
aria-invalid="true"on invalid fields. - Use
role="alert"on error container for immediate announcement. - For multiple errors: error summary at top with links to fields; move focus to summary on validation failure.
- On success: announce via
role="status". - On failure: do not clear the form -- preserve user input.
Input Types and Autocomplete
Use semantic type attributes (email, tel, url, number, password). Use autocomplete for user data inputs (WCAG 1.3.5): given-name, family-name, email, tel, street-address, etc.
Disabling Controls
- Native
disabledattribute removes from tab sequence and announces state. aria-disabled="true"keeps element focusable -- prevent activation in JS.- Announce dynamic disabled state changes via a live region.
---
5. Visual Requirements
- Text contrast: 4.5:1 minimum (3:1 for large text 18pt+ or 14pt bold+).
- UI component contrast: 3:1 minimum for borders, icons, focus indicators.
- Never color alone to convey information -- add icons, text, or patterns.
- Content must reflow at 320px width without horizontal scroll (WCAG 1.4.10).
- No content loss when user adjusts line-height/spacing (WCAG 1.4.12).
- Content on hover/focus must be dismissible, hoverable, persistent (1.4.13).
- No content flashes more than 3 times/second (2.3.1).
- Time limits adjustable; moving content pausable (2.2.1, 2.2.2).
---
6. Component Patterns
For full ARIA structure, keyboard contracts, and code examples for each pattern, see `component-patterns.md`. Key rules:
- Dialog -- use native
<dialog>with.showModal()when possible. Focus trap, Escape to close, return focus to
trigger on close.
- Tabs --
role="tablist/tab/tabpanel", roving tabindex, arrow keys switch tabs, Tab enters active panel. - Accordion -- trigger is
<button>inside a heading,aria-expandedon button,aria-controlsto panel. - Disclosure --
<button>witharia-expandedandaria-controls. - Menu button --
aria-haspopup="true", arrows navigate, Enter activates. Neverrole="menu"for navigation -- use
<nav> with links.
- Combobox --
aria-activedescendanttracks highlighted option, arrows navigate, Enter selects, Escape closes. - Tooltip -- show on focus + hover,
aria-describedby, must be hoverable and persistent. No interactive content
inside.
- Alert/Status --
role="alert"for urgent (assertive),role="status"for non-urgent (polite). Inject into
pre-existing container.
For WCAG 2.2 AA compliance checklist with criterion numbers, see `wcag.md`.
---
Application
When writing accessible code:
- Apply all conventions silently -- do not narrate each ARIA attribute or WCAG criterion being followed.
- Use native HTML elements before reaching for ARIA.
- Include keyboard interaction handlers for every custom interactive widget.
- If an existing codebase contradicts a convention, follow the codebase pattern and flag the divergence once.
When reviewing code for accessibility:
- Cite the specific violation and show the fix inline.
- Reference the WCAG criterion number when relevant (e.g., "1.4.3 Contrast").
- Do not lecture -- state what is wrong and how to fix it.
Integration
The coding skill governs workflow; this skill governs accessibility choices. For CSS-related accessibility (contrast, focus styles, motion), the css skill complements this one.
{
"sources": {
"MDN - Understanding WCAG": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/accessibility/guides/understanding_wcag/index.md",
"MDN - WCAG Perceivable": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/accessibility/guides/understanding_wcag/perceivable/index.md",
"MDN - ARIA Overview": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/accessibility/aria/index.md",
"MDN - ARIA Roles Reference": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/accessibility/aria/reference/roles/index.md",
"MDN - ARIA Attributes Reference": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/accessibility/aria/reference/attributes/index.md",
"MDN - HTML Accessibility Fundamentals": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/learn_web_development/core/accessibility/html/index.md",
"MDN - WAI-ARIA Basics": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/learn_web_development/core/accessibility/wai-aria_basics/index.md",
"MDN - Keyboard Accessibility": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/accessibility/guides/understanding_wcag/keyboard/index.md",
"W3C APG - Keyboard Interface Practices": "https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/",
"W3C APG - Landmark Regions": "https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/",
"W3C APG - Patterns Overview": "https://www.w3.org/WAI/ARIA/apg/patterns/",
"W3C APG - Dialog Modal Pattern": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/",
"W3C - ARIA in HTML": "https://www.w3.org/TR/html-aria/",
"WebAIM - WCAG 2 Checklist": "https://webaim.org/standards/wcag/checklist",
"web.dev - Learn Accessibility Focus": "https://web.dev/learn/accessibility/focus"
},
"lastFetched": "2026-02-16T15:46:16.397Z"
}
ARIA Roles, States, and Properties
ARIA (Accessible Rich Internet Applications) adds semantic meaning to elements when native HTML alone is insufficient. The cardinal rule:
No ARIA is better than bad ARIA.
ARIA modifies only the accessibility tree -- it does not change element behavior, keyboard interaction, or visual appearance. You must implement those yourself.
The Five Rules of ARIA
- Use native HTML first. If an HTML element with the semantics you need exists, use it instead of ARIA.
- Do not change native semantics unless absolutely necessary. Never
<h2 role="tab">-- use
<div role="tab"><h2>...</h2></div>.
- All interactive ARIA controls must be keyboard operable. A
role="button"must respond to Enter and Space. - Do not use `role="presentation"` or `aria-hidden="true"` on focusable elements. This hides them from assistive
technology while they remain keyboard reachable.
- All interactive elements must have an accessible name. Use visible labels,
aria-label, oraria-labelledby.
Roles
Landmark Roles
Prefer HTML elements over ARIA landmark roles. Use ARIA only when the HTML equivalent does not exist or is not supported.
banner→<header>(top-level) — use ARIA for legacy browser supportnavigation→<nav>— use ARIA for legacy browser supportmain→<main>— use ARIA for legacy browser supportcomplementary→<aside>— use ARIA for legacy browser supportcontentinfo→<footer>(top-level) — use ARIA for legacy browser supportsearch→<search>— use ARIA for browsers without<search>supportform→<form>(with accessible name) — use ARIA when<form>lacks a nameregion→<section>(with accessible name) — use ARIA when<section>lacks a name
Widget Roles
Use these only when no native HTML equivalent exists:
tablist,tab,tabpanel— tab interface; arrow keys between tabs, Tab into paneldialog— modal/non-modal dialog; focus trap, Escape to closealertdialog— urgent dialog requiring response; same as dialogmenu,menuitem— action menu (not navigation); arrow keys, Enter to activatemenubar— horizontal menu bar; arrow keys, Enter for submenustree,treeitem— hierarchical list; arrow keys expand/collapsecombobox— text input with popup; arrow keys in list, Escape to closelistbox,option— selection list; arrow keys, Enter to selectslider— range input; arrow keys adjust valueswitch— on/off toggle; Space to toggletoolbar— grouped controls; arrow keys between controlstooltip— descriptive popup; appears on focus and hover
Live Region Roles
alert— assertive, atomic; use for error messages, urgent warningsstatus— polite, atomic; use for success messages, status updateslog— polite, additions relevant; use for chat messages, activity logstimer— off by default; use for countdown displaysmarquee— off by default; use for stock tickers, news feeds
Key Attributes
Naming and Describing
aria-labelledby— names element by referencing another element's ID; 1st priority (overrides all others)aria-label— names element with a string; 2nd priority (when no visible label exists)aria-describedby— adds supplementary description; use after name is establishedaria-description— string description (no ID reference needed); use after name is established
Naming priority order: aria-labelledby > aria-label > <label> > element content > title attribute.
Rules:
- Prefer visible labels (
<label>, element content) overaria-label. - Use
aria-labelledbyto compose names from multiple elements. aria-labelandaria-labelledbyoverride native label text -- do not use them to supplement; they replace.- Do not use
aria-labelon elements withrole="presentation"orrole="none".
State Attributes
aria-expanded(true/false) — disclosure, accordion, menu triggeraria-selected(true/false) — tab, option, grid cellaria-checked(true/false/mixed) — checkbox, switch, radioaria-pressed(true/false/mixed) — toggle buttonaria-disabled(true/false) — non-interactive state (still focusable)aria-hidden(true/false) — remove from accessibility treearia-invalid(true/false/grammar/spelling) — form validationaria-required(true/false) — required field indicatoraria-current(page/step/location/date/time/true) — current item in a setaria-busy(true/false) — region is updating
Relationship Attributes
aria-controls— this element controls that elementaria-owns— this element owns that element (virtual parent/child)aria-flowto— next element in reading orderaria-activedescendant— virtual focus within a composite widgetaria-errormessage— points to error message element for this input
Live Region Attributes
aria-live(off/polite/assertive) — announce content changesaria-atomic(true/false) — re-read entire region or just changesaria-relevant(additions/removals/text/all) — which changes to announce
Rules:
- Use
aria-live="polite"for non-urgent updates (search results, status). - Use
aria-live="assertive"sparingly -- only for errors, alerts, urgent info. - Set live regions in the DOM before content changes. Adding
aria-liveand content simultaneously may not be
announced.
- Use
role="status"(implicitaria-live="polite") for status messages. - Use
role="alert"(implicitaria-live="assertive") for error messages.
Common ARIA Mistakes
<div role="button">without keyboard handler — not keyboard accessible; use<button>or add keydown for
Enter/Space
aria-hidden="true"on focusable element — hidden from AT but still focusable; remove from tab order or remove
aria-hidden
<nav role="navigation">— redundant; remove the role attributearia-labelon<div>without a role — ignored by most AT; add an appropriate role or use a semantic elementaria-expandedon the panel — should be on the trigger; move to the button/trigger element- Missing
aria-controlson tab — tab/panel relationship unclear; addaria-controlspointing to panel ID
Accessible Component Patterns
Patterns for building accessible interactive widgets following the WAI-ARIA Authoring Practices Guide (APG). Each pattern defines the required ARIA roles, states, properties, and keyboard interactions.
Dialog (Modal)
A window overlaid on the primary content. Users cannot interact with content outside the dialog while it is open.
Structure
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
>
<h2 id="dialog-title">Confirm deletion</h2>
<p id="dialog-desc">This action cannot be undone.</p>
<button>Cancel</button>
<button>Delete</button>
</div>Requirements
- Role —
role="dialog"on container - Modal —
aria-modal="true" - Label —
aria-labelledbypointing to visible title - Description —
aria-describedbyfor simple dialog messages - Focus on open — move to first focusable element (or dialog title for complex content)
- Focus trap — Tab/Shift+Tab cycles within dialog only
- Escape — closes the dialog
- Focus on close — returns to the element that triggered the dialog
- Inert background — prevent interaction with content behind the dialog
- Close button — include a visible close/cancel button
Native <dialog>
The HTML <dialog> element with .showModal() provides focus trapping, Escape handling, and inert background natively. Prefer it over custom implementations.
<dialog id="confirm-dialog">
<h2>Confirm deletion</h2>
<p>This action cannot be undone.</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>Tabs
A set of layered panels where one panel is visible at a time.
Structure
<div role="tablist" aria-label="Account settings">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
Profile
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2"
tabindex="-1">
Security
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<!-- Profile content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<!-- Security content -->
</div>Requirements
- Container —
role="tablist"witharia-label - Tabs —
role="tab"on each tab, inside/owned by tablist - Panels —
role="tabpanel"linked viaaria-controls/aria-labelledby - Selected —
aria-selected="true"on active tab - Arrow keys — Left/Right (horizontal) or Up/Down (vertical) to switch tabs
- Tab key — moves focus into the active panel, not to the next tab
- Home/End — move to first/last tab
- Roving tabindex — active tab:
tabindex="0", others:tabindex="-1" - Orientation — add
aria-orientation="vertical"if tabs are stacked vertically
Accordion
Vertically stacked headings that expand/collapse associated content panels.
Structure
<div>
<h3>
<button
aria-expanded="true"
aria-controls="section-1"
id="header-1"
>
Section Title
</button>
</h3>
<div id="section-1" role="region" aria-labelledby="header-1">
<!-- Content -->
</div>
</div>Requirements
- Trigger —
<button>inside a heading element - State —
aria-expanded="true"/"false"on the button - Association —
aria-controlspointing to the panel ID - Panel role —
role="region"witharia-labelledbyreferencing the header - Enter/Space — toggle expanded/collapsed
- Optional — arrow keys between accordion headers, Home/End for first/last
Disclosure (Show/Hide)
A button that toggles visibility of a related content section. Simpler than accordion -- no heading requirement, no multi-panel coordination.
<button aria-expanded="false" aria-controls="details-1">
More details
</button>
<div id="details-1" hidden>
<!-- Hidden content -->
</div>Toggle aria-expanded and the hidden attribute together.
Menu and Menu Button
Action menus (not navigation menus). Use for lists of commands/actions.
Menu Button
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="action-menu"
>
Actions
</button>
<ul role="menu" id="action-menu" hidden>
<li role="menuitem">Edit</li>
<li role="menuitem">Duplicate</li>
<li role="separator"></li>
<li role="menuitem">Delete</li>
</ul>Requirements
- Trigger — button with
aria-haspopup="true"andaria-expanded - Menu —
role="menu"on the container - Items —
role="menuitem"on each item - Enter/Space on trigger — opens menu, focuses first item
- Arrow Down on trigger — opens menu, focuses first item
- Arrow Up/Down — navigate menu items
- Enter — activate focused item and close
- Escape — close menu, return focus to trigger
- Tab — close menu, move focus to next element after trigger
- Home/End — first/last menu item
Do not use `role="menu"` for navigation menus. Use <nav> with a list of links instead.
Combobox (Autocomplete)
A text input with an associated popup list of suggestions.
Structure
<label for="city-input">City</label>
<div>
<input
id="city-input"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="city-listbox"
aria-activedescendant=""
/>
<ul role="listbox" id="city-listbox" hidden>
<li role="option" id="opt-1">New York</li>
<li role="option" id="opt-2">Los Angeles</li>
</ul>
</div>Requirements
- Input —
role="combobox",aria-expanded,aria-autocomplete - List —
role="listbox"withrole="option"children - Association —
aria-controlson input pointing to listbox - Active option —
aria-activedescendanton input pointing to highlighted option - Arrow Down — open list if closed; navigate down in list
- Arrow Up — navigate up in list
- Enter — select highlighted option, close list
- Escape — close list, clear selection highlight
- Typing — filter options and update list
Tooltip
A popup that displays information when an element receives keyboard focus or mouse hover.
<button aria-describedby="tip-1">Save</button>
<div role="tooltip" id="tip-1">Save your changes (Ctrl+S)</div>Requirements
- Show on focus and hover; hide on blur, mouse leave, and Escape.
- Use
aria-describedbyto associate the tooltip with its trigger. - Tooltip content must be hoverable (user can move pointer to tooltip).
- Tooltip must persist until dismissed by user action.
- Do not put interactive content inside tooltips.
Alert
A brief, important message that does not require user action.
<div role="alert">Your changes have been saved.</div>role="alert"impliesaria-live="assertive"andaria-atomic="true".- Inject the alert element into the DOM (or update its content) to trigger announcement. Having
role="alert"in the
DOM before content is added is the most reliable pattern.
- Use sparingly -- assertive announcements interrupt the current screen reader output.
Status Message
A non-urgent update that does not move focus.
<div role="status">3 results found.</div>role="status"impliesaria-live="polite"andaria-atomic="true".- Use for search result counts, form submission success, progress updates.
Accessible Forms
Forms are among the most critical accessibility touchpoints. Every form control must be labeled, every error must be announced, and every interaction must work without a mouse.
Labels
Every form control must have a programmatically associated label.
Labeling Methods (in priority order)
1. `<label>` with `for`/`id` -- preferred for all visible labels. 2. Wrapping `<label>` -- wraps the control inside the label element. 3. `aria-labelledby` -- references another element's ID as the label. 4. `aria-label` -- string label when no visible label exists.
<!-- Method 1: explicit for/id (preferred) -->
<label for="username">Username</label>
<input id="username" name="username" type="text" />
<!-- Method 2: wrapping label -->
<label>
Username
<input name="username" type="text" />
</label>
<!-- Method 3: aria-labelledby (for complex layouts) -->
<span id="qty-label">Quantity</span>
<input aria-labelledby="qty-label" type="number" />
<!-- Method 4: aria-label (no visible label) -->
<input aria-label="Search" type="search" />Rules:
- Every
<input>,<select>, and<textarea>must have a label. placeholderis not a substitute for a label -- it disappears when the user types and is not reliably read by all
screen readers.
- The visible label text must be contained in the accessible name (WCAG 2.5.3 Label in Name) -- if the visible text says
"Email", the aria-label must include "Email", not "Enter your electronic mail".
Grouping
Fieldset and Legend
Group related controls with <fieldset> and <legend>. The legend text becomes part of the accessible name for each control in the group.
<fieldset>
<legend>Shipping address</legend>
<label for="street">Street</label>
<input id="street" name="street" type="text" />
<label for="city">City</label>
<input id="city" name="city" type="text" />
</fieldset>Required for:
- Radio button groups
- Checkbox groups with a shared label
- Sets of related inputs (address fields, date parts)
Group Role
Use role="group" with aria-labelledby when <fieldset> is impractical (e.g., inside a flex or grid layout where fieldset styling causes issues).
Required Fields
- Mark required fields with
requiredattribute (native validation) oraria-required="true"(custom validation). - Indicate required fields visually -- asterisk with a legend explaining "\* Required" is the standard pattern.
- Do not rely on color alone to indicate required status.
<p><span aria-hidden="true">*</span> indicates a required field</p>
<label for="name">Full name <span aria-hidden="true">*</span></label>
<input id="name" name="name" type="text" required />Validation and Error Messages
Client-Side Validation
- Display errors adjacent to the invalid field.
- Associate each error with its field using
aria-describedbyoraria-errormessage. - Set
aria-invalid="true"on invalid fields. - Use
role="alert"oraria-live="assertive"on the error container for dynamic error messages so they are announced
immediately.
<label for="email">Email</label>
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid="true"
/>
<span id="email-error" role="alert">
Please enter a valid email address.
</span>Error Summary
For forms with multiple errors, provide an error summary at the top:
<div role="alert" aria-labelledby="error-heading">
<h2 id="error-heading">2 errors found</h2>
<ul>
<li><a href="#email">Email: Please enter a valid email</a></li>
<li><a href="#phone">Phone: Required field</a></li>
</ul>
</div>Move focus to the error summary when validation fails. The links inside allow users to jump directly to each invalid field.
Submission Feedback
- On success, announce the result with
role="status"oraria-live="polite". - On failure, move focus to the error summary or first invalid field.
- Do not clear the form on failure -- preserve user input.
Input Types and Autocomplete
Use appropriate type attributes for semantic input behavior:
type="email"-- email keyboard on mobile, basic validationtype="tel"-- numeric keyboard on mobiletype="url"-- URL keyboard on mobiletype="number"-- numeric input with spinnertype="password"-- masked input with potential password manager
Use autocomplete attributes to help browsers and assistive technology identify input purpose (WCAG 1.3.5):
<input type="text" autocomplete="given-name" />
<input type="text" autocomplete="family-name" />
<input type="email" autocomplete="email" />
<input type="tel" autocomplete="tel" />
<input type="text" autocomplete="street-address" />Disabling Controls
- Use the native
disabledattribute on form elements -- it removes them from the tab sequence and announces the
disabled state.
- When using
aria-disabled="true"instead, the element remains focusable and announced but you must prevent activation
in JavaScript.
- When a control's disabled state changes dynamically, announce the change via a live region so screen reader users are
informed.
Keyboard Navigation and Focus Management
All interactive functionality must be operable with a keyboard alone. This is non-negotiable -- it enables access for screen reader users, switch device users, voice control users, and anyone without a pointing device.
Fundamental Conventions
- Tab moves focus between interactive components (links, buttons, inputs).
- Shift+Tab moves focus backward.
- Arrow keys move focus within composite widgets (tabs, menus, grids).
- Enter activates links, buttons, and menu items.
- Space activates buttons, checkboxes, and toggles.
- Escape closes overlays, menus, and dialogs.
Focus Order
Focus order must match visual reading order. The DOM order determines the default tab sequence.
Rules:
- Never use
tabindexvalues greater than 0. Rearrange DOM order instead. - Use
tabindex="0"to make non-interactive elements focusable (sparingly). - Use
tabindex="-1"for elements that should be programmatically focusable but not in the tab sequence (e.g., dialog
containers, skip link targets).
- Source order = visual order = focus order. CSS layout changes (
flex-direction: row-reverse,order,grid
reordering) must not break this alignment.
Focus Visibility
Every focusable element must have a visible focus indicator.
Rules:
- Never use
outline: noneoroutline: 0without providing a custom focus style. - Use
:focus-visiblefor keyboard-only focus styles (avoids showing outlines on mouse click). - Focus indicator must have at least 3:1 contrast against the adjacent background.
- The focus indicator area must be at least as large as a 2px border around the element (WCAG 2.2 AAA 2.4.13).
/* Good: custom focus indicator with :focus-visible */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* Good: high-contrast focus for dark backgrounds */
.dark-section :focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}
/* Bad: removed focus with no replacement */
*:focus {
outline: none; /* NEVER DO THIS */
}Skip Links
Provide a skip link as the first focusable element on the page to bypass repeated navigation.
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main-content"><!-- content --></main>
</body>.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}Focus Management Patterns
Roving Tabindex
For composite widgets (tabs, toolbars, menus) where only one child should be in the tab sequence at a time:
1. Set tabindex="0" on the active/current element. 2. Set tabindex="-1" on all other focusable children. 3. On arrow key press: move tabindex="0" to the target, set previous to tabindex="-1", and call .focus() on the new element.
function handleArrowKey(items, currentIndex, direction) {
const nextIndex = direction === "next"
? (currentIndex + 1) % items.length
: (currentIndex - 1 + items.length) % items.length;
items[currentIndex].setAttribute("tabindex", "-1");
items[nextIndex].setAttribute("tabindex", "0");
items[nextIndex].focus();
}aria-activedescendant
Alternative to roving tabindex for composite widgets. The container keeps DOM focus, and aria-activedescendant tells assistive technology which child is "virtually" focused.
1. Container element has tabindex="0" and keeps DOM focus. 2. Set aria-activedescendant to the ID of the visually focused child. 3. Update on arrow key navigation.
When to use which:
- Roving tabindex: scrolls focused element into view automatically.
aria-activedescendant: better when the container must maintain focus (e.g., combobox text input).
Dialog Focus Trap
When a modal dialog opens:
1. Move focus to the first focusable element inside the dialog (or the dialog element itself if content is complex). 2. Trap Tab/Shift+Tab within the dialog -- wrap from last to first and vice versa. 3. Close on Escape key. 4. On close, return focus to the element that opened the dialog. 5. Set aria-modal="true" on the dialog container.
function trapFocus(dialogElement) {
const focusable = dialogElement.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), ' +
'select:not([disabled]), textarea:not([disabled]), [tabindex="0"]'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
dialogElement.addEventListener("keydown", (e) => {
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === "Escape") {
closeDialog();
}
});
first.focus();
}Focus Restoration
When dynamic content is removed (deleted list item, closed dialog, dismissed notification), focus must move to a logical target:
- Deleted item -- focus the next item, or previous if last was deleted.
- Closed overlay -- focus the trigger element that opened it.
- Removed section -- focus the nearest logical container or heading.
Never let focus fall to document.body -- this causes a complete loss of keyboard context.
Keyboard Patterns by Widget
- Button — Enter, Space: activate
- Link — Enter: navigate
- Checkbox — Space: toggle checked state
- Radio group — Arrow keys: move selection between options
- Tabs — Arrow keys: switch active tab
- Menu — Arrow keys, Enter: navigate items, activate
- Dialog — Tab (trapped), Escape: navigate within, close
- Accordion — Enter/Space: toggle expanded section
- Combobox — Arrow keys, Enter, Escape: navigate list, select, close
- Slider — Arrow keys: adjust value
- Tree view — Arrow keys: navigate, expand/collapse
Disabled Elements
- Remove disabled standalone controls from the tab sequence (
disabledattribute on native elements, ortabindex="-1"
for custom).
- Keep disabled items focusable inside composite widgets (menu items, tabs, tree items, listbox options) so screen
reader users can discover them.
- Use
aria-disabled="true"when you want the element to remain focusable and announced as disabled, but not operable.
Semantic HTML for Accessibility
Semantic HTML is the foundation of accessible web content. Native elements provide built-in keyboard support, screen reader announcements, and focus management that ARIA can only approximate.
The Core Principle
Use the right element for the right job. A <button> gives you keyboard activation, focus management, and screen reader announcements for free. A <div role="button"> requires you to manually implement all of that.
Page Structure
Landmarks
Use HTML sectioning elements to create landmark regions. Screen readers expose these as navigation shortcuts.
<header>(top-level) →banner— site-wide header, logo, primary nav<nav>→navigation— navigation link groups<main>→main— primary page content (one per page)<aside>→complementary— supporting content related to main<footer>(top-level) →contentinfo— site-wide footer, copyright, links<section>(with accessible name) →region— generic labeled region<search>→search— search functionality container
Rules:
- Every page must have exactly one
<main>. <header>and<footer>only map tobanner/contentinfowhen they are direct children of<body>-- nested
inside <article>, <section>, etc. they have no landmark role.
- If multiple
<nav>elements exist, label each witharia-labeloraria-labelledbyto distinguish them. - Do not duplicate implicit roles -- never write
<main role="main">. - Include all perceivable content within a landmark region.
Headings
Headings (<h1>--<h6>) create a document outline. Screen reader users navigate by headings more than any other method.
Rules:
- One
<h1>per page identifying the primary content. - Do not skip heading levels --
<h1>then<h3>breaks the outline. - Use headings to structure content, not for visual styling.
- Every
<section>and major content area should begin with a heading.
<!-- Good: logical heading hierarchy -->
<h1>Product Catalog</h1>
<h2>Electronics</h2>
<h3>Laptops</h3>
<h3>Phones</h3>
<h2>Clothing</h2>
<!-- Bad: skipped heading level, heading for styling -->
<h1>Products</h1>
<h3>Electronics</h3> <!-- skipped h2 -->
<h5 class="small-heading">Note</h5> <!-- heading for styling -->Interactive Elements
Buttons vs Links
<button>— triggers an action (submit, toggle, open dialog); activated by Enter, Space<a href>— navigates to a URL or anchor; activated by Enter
Never use `<a href="#" onclick="...">` for actions. Use <button>. Never use `<button>` for navigation. Use <a href>.
Form Controls
Native form elements provide built-in accessibility:
<input>,<select>,<textarea>-- keyboard operable, screen reader announced with type and state<fieldset>+<legend>-- groups related controls with a group label<label>+for/idassociation -- connects label text to control
<!-- Good: explicit label association -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" required />
<!-- Good: grouped radio buttons -->
<fieldset>
<legend>Notification preference</legend>
<label><input type="radio" name="notify" value="email" /> Email</label>
<label><input type="radio" name="notify" value="sms" /> SMS</label>
</fieldset>
<!-- Bad: no label association -->
<span>Email</span>
<input type="email" name="email" />Tables
Data tables require proper markup for screen readers to associate cells with headers.
<table>
<caption>Quarterly revenue by region</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>$1.2M</td>
<td>$1.4M</td>
</tr>
</tbody>
</table>Rules:
- Use
<th>withscope="col"orscope="row"for headers. - Add
<caption>to describe the table's purpose. - Never use tables for layout.
Text Alternatives
Images
- Informative — describe the content and function
- Decorative —
alt=""(empty string, not omitted) - Functional (link/button) — describe the action, not the image
- Complex (chart/graph) — brief
alt+ detailed description nearby
<!-- Informative -->
<img src="chart.png" alt="Sales increased 25% from Q1 to Q4 2025" />
<!-- Decorative -->
<img src="divider.png" alt="" />
<!-- Functional -->
<a href="/home"><img src="logo.png" alt="Acme Corp home page" /></a>
<!-- Complex: brief alt + linked description -->
<figure>
<img src="org-chart.png" alt="Organization structure" aria-describedby="org-desc" />
<figcaption id="org-desc">CEO at top, three VPs reporting...</figcaption>
</figure>Rules:
- Every
<img>must have analtattribute -- even if empty. - Do not start alt text with "Image of" or "Picture of" -- screen readers already announce it as an image.
- Keep alt text concise -- typically under 125 characters.
- For decorative images, prefer CSS
background-imageover<img alt="">.
Lists
Use semantic list elements for groups of related items:
<ul>for unordered lists<ol>for ordered/sequential lists<dl>,<dt>,<dd>for term/description pairs
Screen readers announce list type and item count, enabling users to decide whether to traverse or skip.
Language
- Set the page language:
<html lang="en">. - Mark language changes inline:
<span lang="fr">bonjour</span>. - Use clear, simple language. Expand abbreviations on first use with
<abbr>.
WCAG 2.2 Reference
The Web Content Accessibility Guidelines (WCAG) 2.2 define success criteria organized under four principles: Perceivable, Operable, Understandable, and Robust. Each criterion has a conformance level: A (minimum), AA (standard target), or AAA (enhanced).
Target AA conformance unless the project explicitly specifies otherwise. AA includes all A criteria.
Perceivable
Content must be presentable in ways users can perceive.
Text Alternatives (1.1)
| Criterion | Level | Requirement |
|---|---|---|
| 1.1.1 Non-text Content | A | All non-text content has a text alternative. Decorative images use alt="". |
Time-Based Media (1.2)
| Criterion | Level | Requirement |
|---|---|---|
| 1.2.1 Audio/Video Only | A | Transcripts for audio-only; transcript or audio description for video-only |
| 1.2.2 Captions | A | Synchronized captions for prerecorded video with audio |
| 1.2.3 Audio Description or Transcript | A | Audio description or text transcript for prerecorded video |
| 1.2.4 Captions (Live) | AA | Captions for live audio content |
| 1.2.5 Audio Description | AA | Audio description for prerecorded video |
Adaptable (1.3)
| Criterion | Level | Requirement |
|---|---|---|
| 1.3.1 Info and Relationships | A | Structure/relationships conveyed programmatically (headings, labels, lists) |
| 1.3.2 Meaningful Sequence | A | Reading order determinable from DOM order |
| 1.3.3 Sensory Characteristics | A | Instructions do not rely solely on shape, size, position, or sound |
| 1.3.4 Orientation | AA | Content works in both portrait and landscape |
| 1.3.5 Identify Input Purpose | AA | Use autocomplete for user data inputs |
Distinguishable (1.4)
| Criterion | Level | Requirement |
|---|---|---|
| 1.4.1 Use of Color | A | Color is not the sole means of conveying information |
| 1.4.2 Audio Control | A | Mechanism to pause/stop/mute auto-playing audio > 3s |
| 1.4.3 Contrast (Minimum) | AA | Text: 4.5:1; Large text (18pt+ or 14pt bold+): 3:1 |
| 1.4.4 Resize Text | AA | Page usable at 200% zoom |
| 1.4.5 Images of Text | AA | Use real text, not images of text |
| 1.4.10 Reflow | AA | No horizontal scroll at 320px width (400% zoom at 1280px) |
| 1.4.11 Non-text Contrast | AA | UI components and graphical objects: 3:1 contrast |
| 1.4.12 Text Spacing | AA | No content loss when user adjusts line-height, spacing |
| 1.4.13 Content on Hover/Focus | AA | Dismissible, hoverable, persistent |
Operable
UI components and navigation must be operable.
Keyboard (2.1)
| Criterion | Level | Requirement |
|---|---|---|
| 2.1.1 Keyboard | A | All functionality available via keyboard |
| 2.1.2 No Keyboard Trap | A | Focus can always be moved away from any component |
| 2.1.4 Character Key Shortcuts | A | Single-character shortcuts can be disabled or remapped |
Enough Time (2.2)
| Criterion | Level | Requirement |
|---|---|---|
| 2.2.1 Timing Adjustable | A | Time limits can be turned off, adjusted, or extended |
| 2.2.2 Pause, Stop, Hide | A | Moving/blinking content can be paused; auto-updating can be controlled |
Seizures (2.3)
| Criterion | Level | Requirement |
|---|---|---|
| 2.3.1 Three Flashes | A | Nothing flashes more than 3 times per second |
Navigable (2.4)
| Criterion | Level | Requirement |
|---|---|---|
| 2.4.1 Bypass Blocks | A | Skip navigation mechanism provided |
| 2.4.2 Page Titled | A | Descriptive page <title> |
| 2.4.3 Focus Order | A | Focus order is logical and intuitive |
| 2.4.4 Link Purpose (In Context) | A | Link purpose determinable from link text or context |
| 2.4.5 Multiple Ways | AA | Multiple ways to locate pages (search, sitemap, nav) |
| 2.4.6 Headings and Labels | AA | Headings and labels are descriptive |
| 2.4.7 Focus Visible | AA | Keyboard focus indicator is visible |
| 2.4.11 Focus Not Obscured (Minimum) | AA | Focused element is not entirely hidden by other content |
Input Modalities (2.5)
| Criterion | Level | Requirement |
|---|---|---|
| 2.5.1 Pointer Gestures | A | Multipoint gestures have single-pointer alternatives |
| 2.5.2 Pointer Cancellation | A | Down-event activation can be aborted |
| 2.5.3 Label in Name | A | Accessible name contains the visible label text |
| 2.5.4 Motion Actuation | A | Motion-triggered functions have UI alternatives |
| 2.5.7 Dragging Movements | AA | Drag operations have click alternatives |
| 2.5.8 Target Size (Minimum) | AA | Pointer targets are at least 24x24px (with spacing exceptions) |
Understandable
Content and operation must be understandable.
Readable (3.1)
| Criterion | Level | Requirement |
|---|---|---|
| 3.1.1 Language of Page | A | <html lang="..."> set correctly |
| 3.1.2 Language of Parts | AA | Language changes marked with lang attribute |
Predictable (3.2)
| Criterion | Level | Requirement |
|---|---|---|
| 3.2.1 On Focus | A | Receiving focus does not trigger unexpected changes |
| 3.2.2 On Input | A | Changing input does not trigger unexpected changes |
| 3.2.3 Consistent Navigation | AA | Navigation order is consistent across pages |
| 3.2.4 Consistent Identification | AA | Same-function components are consistently identified |
| 3.2.6 Consistent Help | A | Help mechanisms appear in consistent locations |
Input Assistance (3.3)
| Criterion | Level | Requirement |
|---|---|---|
| 3.3.1 Error Identification | A | Errors identified and described in text |
| 3.3.2 Labels or Instructions | A | Labels or instructions provided for input |
| 3.3.3 Error Suggestion | AA | Suggested corrections for detected errors |
| 3.3.4 Error Prevention (Legal, Financial) | AA | Submissions are reversible, checked, or confirmed |
| 3.3.7 Redundant Entry | A | Previously entered info auto-populated or selectable |
| 3.3.8 Accessible Authentication | AA | No cognitive function test required for auth |
Robust
Content must be robust enough for diverse user agents and assistive technology.
Compatible (4.1)
| Criterion | Level | Requirement |
|---|---|---|
| 4.1.2 Name, Role, Value | A | Custom components expose name, role, and state via ARIA |
| 4.1.3 Status Messages | AA | Status messages announced without receiving focus |
Quick Compliance Checklist (AA)
When writing or reviewing code, verify:
- [ ] All images have appropriate
alttext - [ ] All form controls have associated labels
- [ ] Heading hierarchy is logical (no skipped levels)
- [ ] Color contrast meets minimums (4.5:1 text, 3:1 large text, 3:1 UI)
- [ ] All functionality is keyboard accessible
- [ ] Focus order matches visual order
- [ ] Focus indicator is visible
- [ ] Skip link is present
- [ ] Page has a descriptive
<title>andlangattribute - [ ] Error messages are associated with their fields
- [ ] Status messages use live regions
- [ ] Content reflows at 320px width without horizontal scroll
- [ ] Touch targets are at least 24x24px