mcpField
W
web-accessibility-web-accessibility

web-accessibility-web-accessibility

github.com/agents-incDesign & UI26103 installsUpdated 2d ago
Repository

About web-accessibility-web-accessibility

A practical guide to building accessible web interfaces for designers and frontend developers. It focuses on WCAG 2.2 AA, covering semantic HTML, keyboard interaction, focus, contrast, and assistive-technology testing.

Key features

  1. 01Prefer semantic HTML
  2. 02Check keyboard access and focus
  3. 03Name controls for accessibility
  4. 04Meet text and UI contrast requirements
  5. 05Handle dynamic announcements and color-independent status

Use cases

  1. 01Design keyboard-operable pages
  2. 02Provide accessible form fields and errors
  3. 03Manage focus in dialogs
  4. 04Review interfaces against WCAG 2.2 AA

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/agents-inc/skills --skill web-accessibility-web-accessibility

After installing, type/web-accessibility-web-accessibilityin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

metadata.yaml
reference.md
examples/
color.md
core.md
focus.md
forms.md
screen-reader.md
tables.md
testing.md
touch-targets.md

Details

From the project README

Accessibility

Quick Guide: Most of accessibility is settled by choosing the right element: a <button> arrives with focus, keyboard activation and a role that a <div> needs a dozen lines to fake. What the element cannot give you is the rest — a name for every control, a visible focus indicator, 4.5:1 contrast on text, an announcement when content changes, and information never carried by colour alone. Target WCAG 2.2 Level AA. Automated checks find roughly half the failures, so a keyboard pass is not optional.

Detailed Resources:


<critical_requirements>

Before writing UI code

Reach for the semantic element before the ARIA attribute. <button>, <a href>, <nav> and <table> arrive with a role, keyboard behaviour and focus already correct — every one of which has to be rebuilt by hand on a <div>, and the rebuild is where the failures are.

Make every interactive element reachable and visibly focused. Keyboard users navigate by what the focus ring tells them, so outline: none without a replacement leaves them moving blind through the page.

Give every control a name a screen reader can announce. An icon-only button reads as "button" without one, which tells the listener there is something there and nothing about what it does.

Hold text to 4.5:1 against its background, and UI boundaries and focus indicators to 3:1. Below that the text is unreadable in bright light, on a cheap panel, or to anyone with reduced contrast sensitivity — which is most people eventually.

Pair every colour signal with a shape, an icon or a word. Red-for-error carries nothing to a red-green colour-blind reader, and nothing at all through a screen reader.

Announce content that appears without a page change. A validation error, a saved confirmation and a loaded result are all silent unless they arrive in a live region.

</critical_requirements>


Auto-detection: accessibility, a11y, WCAG, ARIA, aria-label, aria-labelledby, aria-describedby, aria-live, aria-expanded, aria-invalid, role attribute, keyboard navigation, focus management, focus trap, focus-visible, skip link, landmark, screen reader, sr-only, colour contrast, prefers-reduced-motion, tabindex, axe

Applies to:

  • Keyboard reachability, tab order, focus movement and focus indicators
  • ARIA roles, states and properties, and when the element makes them unnecessary
  • Contrast ratios and colour-independent information
  • Accessible names, live regions and announcement timing
  • Target size, motion preferences, and the WCAG 2.2 criteria that added them

Handled elsewhere:

  • Component internals for complex widgets — a tested primitive library implements the ARIA contract below; this skill states the contract so you can check whichever one you use.
  • Visual design tokens — the palette is designed elsewhere, and this skill supplies the ratio it has to clear.
  • Test runner mechanics — running an audit is your test tooling's job, and what to assert is here.
  • Content authoring — reading level, plain language and caption text are writing decisions rather than markup ones.

Philosophy

An accessible interface is not a variant of the interface. It is the same one, built out of elements that already carry meaning, with the parts a browser cannot infer stated explicitly.

That reframes most of the work as subtraction. Semantic HTML is the accessible baseline; every <div> that replaces a real element is a debt paid back in ARIA attributes, key handlers and focus management, and paid back badly. The attributes below exist for the cases where no element carries the meaning — not as a layer applied over markup that could have carried it.

Build for the keyboard first. Everything a pointer can do, a keyboard has to do too, and a design that works without a mouse works with one.


<decision_framework>

Which mechanism the case needs

What is being built?
├─ Something that acts when activated → <button>
│   └─ Icon only → add an accessible name
├─ Something that navigates → <a href>
│   └─ Current destination → aria-current="page"
├─ A structural region → <nav>, <main>, <header>, <aside>, <footer>
│   └─ More than one of a kind → label each with aria-label
├─ A complex widget — dialog, listbox, tabs, combobox, tree
│   └─ Take a tested primitive. The ARIA contract is large,
│      and a partial implementation reads as broken rather than absent.
└─ Content that appears without navigation
    ├─ An error or something urgent → role="alert"
    └─ Progress or confirmation → role="status"

ARIA or not: the first rule of ARIA is not to use ARIA. role="button" on a <div> gives a screen reader the right word and gives the keyboard nothing, so the handlers, tabindex and focus styling are all still owed. Reach for ARIA when no element expresses the thing — a live region, a disclosure's aria-expanded, a sort direction on a column header.

AA or AAA: AA is the compliance target and what regulation generally references. Individual AAA criteria are often cheap — 7:1 contrast, 44×44 targets — and worth taking where the design allows, without committing the whole product to AAA.

</decision_framework>


Core patterns

Pattern 1: Skip link

The first focusable element on the page, hidden until it takes focus, jumping past the navigation every page repeats.

<a href="#main-content" className="skip-link">Skip to main content</a>
…
<main id="main-content" tabIndex={-1}>{children}</main>

tabIndex={-1} on the target is what lets focus actually land there; without it the fragment moves the viewport and leaves focus at the top of the document.

Full code: examples/core.md

Pattern 2: Landmarks

Landmark elements are how a screen reader user jumps between regions rather than reading linearly.

<header>…</header>
<nav aria-label="Main">…</nav>
<main>…</main>
<aside aria-label="Related">…</aside>
<footer>…</footer>

One <main> per page. Where two landmarks share a type, each needs a distinguishing label — two unlabelled <nav>s are announced identically.

Full code: examples/core.md

Pattern 3: Button versus link

Activation is a button; navigation is a link. The distinction decides the keyboard behaviour, the announcement, and whether the browser's own affordances work.

<button onClick={handleSubmit}>Submit form</button>
<a href="/dashboard">Go to dashboard</a>

A <button> that navigates costs the reader "open in new tab", the status-bar URL and the middle click. A <div onClick> costs them the whole interaction.

Full code: examples/core.md

Pattern 4: Accessible names

Every control needs a name, and the icon-only case is where it goes missing.

<button aria-label="Delete item">
  <TrashIcon aria-hidden="true" />
</button>

<button>
  <TrashIcon aria-hidden="true" />
  <span className="sr-only">Delete item</span>
</button>

Both announce "Delete item, button". The second survives translation tooling and shows up in a text search of the DOM, so prefer it where either matters.

Resolution order is in reference.md; full code in examples/screen-reader.md.

Pattern 5: Form fields and errors

A label associated with the input, the invalid state on the input, and the message reachable from it.

<label htmlFor="email">Email</label>
<input
  id="email"
  type="email"
  aria-required="true"
  aria-invalid={Boolean(error)}
  aria-describedby={error ? "email-error" : undefined}
/>
{error && <span id="email-error" role="alert">{error}</span>}

A placeholder is not a label: it disappears the moment typing starts, and it fails contrast in most designs.

Full code: examples/forms.md

Pattern 6: Focus indicators

:focus-visible shows the ring for keyboard interaction and withholds it from a mouse click, which removes the reason people delete focus styles in the first place.

.button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

At least 2px, and at least 3:1 against whatever sits behind it.

Full code: examples/focus.md

Pattern 7: Dialogs and focus movement

The contract a modal owes, whoever implements it: focus moves in on open, cannot leave while it is open, Escape closes it, and focus returns to the element that opened it.

<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <h2 id="dialog-title">{title}</h2>
  …
</div>

Focus returning to the trigger is the half most often missed — without it a keyboard user is dropped back at the top of the document with no idea where they were.

Full code: examples/focus.md

Pattern 8: Live regions

Content that arrives without a navigation is silent unless a live region announces it.

<div role="status">{savedMessage}</div>
<div role="alert">{errorMessage}</div>

role="status" waits for a pause; role="alert" interrupts. The region must be in the DOM before the message arrives — inserting the region and its content together often announces nothing.

Pattern 9: Colour-independent status

Colour is an accent on the signal, never the signal.

<span className={statusClass}>
  <span aria-hidden="true">{status === "error" ? "×" : "✓"}</span>
  <span>{status === "error" ? "Failed" : "Complete"}</span>
</span>

The icon carries it visually and the text carries it to a screen reader; remove the colour and both still work.

Full code: examples/color.md

Pattern 10: Motion preferences

Animate by opting in, so a reader who has asked for less motion gets it by default.

@media (prefers-reduced-motion: no-preference) {
  .card {
    animation: slide-in 300ms ease-out;
  }
}

reduce means minimise rather than eliminate — a fade in place of a slide keeps the feedback and drops the vestibular trigger.


<red_flags>

Red flags

Breaks at runtime:

  • outline: none with no replacement — keyboard users lose their position entirely; scope the ring to :focus-visible instead of removing it.
  • A <div> or <span> with an onClick — not focusable, not activated by Enter or Space, announced as nothing; use a <button>.
  • role="button" on a <div> — supplies the word and none of the behaviour, so Tab, Enter and Space are all still owed.
  • aria-hidden="true" on anything focusable — focus lands on an element the screen reader insists is not there.
  • tabindex above zero — hoists the element ahead of the whole document order, and one such value disorders the entire page.
  • An input with no associated label — announced by its type alone, so a form reads as "edit text, edit text, edit text".

Surprising behaviour:

  • alt="" is the correct value for a decorative image; omitting alt entirely makes a screen reader read the filename.
  • A live region added to the DOM at the same moment as its message usually announces nothing — render the container first.
  • A live region announces only the part that changed, so "3 of 10 results" updating in place can be read as "5" with no context; aria-atomic="true" makes it re-read the whole region.
  • role="alert" interrupts whatever is being read, so a per-keystroke validation message in one is unusable.
  • A placeholder is not a label, and disappears exactly when the reader needs it most.
  • Disabled buttons are skipped by focus, so a submit button disabled until the form is valid gives no route to the reason.
  • A <label> wrapping the input and a for/htmlFor both work; two labels on one input do not, and only one is announced.
  • prefers-reduced-motion: reduce asks for less, not none — removing essential feedback overshoots.
  • Automated audits find roughly half of WCAG failures. Everything about order, wording and whether the announcement made sense needs a person.

</red_flags>

Comments

Markdown supported