Skip to content
Development
Skill

/a11y-fixes

Resolve axe-core accessibility violations reported by Vitest (test/a11y.ts), Playwright (.playwright/a11y.ts), or the code-audit-frontend agent's a11y bucket. Trigger on any axe rule id appearing in test output, not only the ones named here. Contains fix patterns for the most

From plugin
gaia-react-gaia
2320 skills10 agents14 commands
Install
$ npx -y skills add gaia-react/gaia --skill a11y-fixes --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/a11y-fixes

Context preview

The summary Claude sees to decide when to auto-load this skill.

Resolve axe-core accessibility violations reported by Vitest (test/a11y.ts), Playwright (.playwright/a11y.ts), or the code-audit-frontend agent's a11y bucket. Trigger on any axe rule id appearing in test output, not only the ones named here. Contains fix patterns for the most

SKILL.md

a11y-fixes.SKILL.md
name: a11y-fixes
description: Resolve axe-core accessibility violations reported by Vitest (test/a11y.ts), Playwright (.playwright/a11y.ts), or the code-audit-frontend agent's a11y bucket. Trigger on any axe rule id appearing in test output, not only the ones named here. Contains fix patterns for the most common violations (color-contrast, label, label-title-only, image-alt, button-name, link-name, region, landmark-one-main, heading-order, aria-allowed-attr, aria-required-attr, aria-required-children, aria-required-parent, aria-valid-attr-value, focus-trap, tabindex, html-has-lang, document-title, duplicate-id, listitem, definition-list); for any violation not listed, apply the general axe guidance and the same fix-then-verify loop.
model: haiku

Accessibility Fix Patterns

How to resolve specific axe-core violations in this project.

Violations come from `test/a11y.ts` (Vitest), `.playwright/a11y.ts` (Playwright), or the `code-audit-frontend` agent's a11y bucket. General a11y guidance lives in `.claude/rules/accessibility.md`.

color-contrast

WCAG AA requires 4.5:1 for normal text, 3:1 for large text. Use the project's semantic Tailwind tokens (see `.claude/rules/tailwind.md`) instead of arbitrary palette colors, they pair light/dark modes correctly.

// BAD, fails contrast in dark mode, raw colors
<p className="text-gray-400 bg-white">Status</p>

// GOOD, semantic tokens, contrast-safe in both modes
<p className="text-body bg-body">Status</p>

label

Form inputs need an associated `<label>`. Use GAIA's `Field` wrapper from `~/components/Form/Field` rather than a bare `<label>`, it wires `htmlFor`, error text, and description automatically. See the `form-components.md` audit extension.

// BAD, bare input with no label association
<input type="text" name="email" />

// GOOD, Field wraps a project input with the right wiring
<Field type="input" name="email" label={t('email')}>
  <InputText name="email" />
</Field>

label-title-only

`title` attributes are not labels, screen readers and mobile devices ignore them. Use `aria-label` or a real `<label>`.

// BAD, title is not an accessible name
<input type="text" title="Search" />

// GOOD, aria-label provides the accessible name
<input type="text" aria-label={t('search')} />

image-alt

Every `<img>` needs `alt`. Content images describe the image; decorative images use `alt=""`. See `.claude/rules/accessibility.md`.

// BAD, no alt attribute
<img src="/logo.png" />

// GOOD, content image
<img src="/logo.png" alt={t('companyLogo')} />

// GOOD, decorative image, hidden from AT
<img src="/divider.svg" alt="" />

button-name

Buttons need an accessible name. Visible text is fine; icon-only buttons need `aria-label`.

// BAD, icon-only button with no name
<button onClick={onClose}>
  <CloseIcon />
</button>

// GOOD, aria-label supplies the name
<button aria-label={t('close')} onClick={onClose}>
  <CloseIcon />
</button>

// GOOD, visible text, no aria-label needed
<button onClick={onSave}>{t('save')}</button>

link-name

Same pattern as `button-name`, anchors need an accessible name.

// BAD, icon-only link
<Link to="/settings"><GearIcon /></Link>

// GOOD, aria-label on the link
<Link to="/settings" aria-label={t('settings')}>
  <GearIcon />
</Link>

region / landmark-one-main

Page content must live inside a landmark, and there must be exactly one `<main>`. GAIA's Layout component owns the `<main>` landmark, page components render inside it and should not add their own.

// BAD, page component wraps itself in <main>, duplicating Layout's
const Page = () => (
  <main>
    <h1>{t('title')}</h1>
  </main>
);

// GOOD, page renders into Layout's <main>
const Page = () => (
  <>
    <h1>{t('title')}</h1>
  </>
);

heading-order

One `<h1>` per page. Levels do not skip, `h2 → h4` is a violation.

// BAD, skips h3
<h2>{t('section')}</h2>
<h4>{t('subsection')}</h4>

// GOOD, sequential levels
<h2>{t('section')}</h2>
<h3>{t('subsection')}</h3>

aria-allowed-attr

ARIA attributes are role-scoped. Look up the role; only attrs in its allowed list are valid. Common case: `aria-checked` only on `role="checkbox"`, `role="radio"`, `role="menuitemcheckbox"`, `role="menuitemradio"`, `role="switch"`, `role="treeitem"`.

// BAD, aria-checked is not allowed on a button
<button aria-checked={selected}>{t('toggle')}</button>

// GOOD, aria-pressed for buttons, aria-checked for checkbox role
<button aria-pressed={selected}>{t('toggle')}</button>

aria-required-attr

Some roles require companion attrs. Disclosure widgets (`aria-expanded`) need `aria-controls` pointing at the controlled element's id.

// BAD, aria-expanded with no aria-controls
<button aria-expanded={open}>{t('menu')}</button>

// GOOD, aria-controls names the panel
<button aria-expanded={open} aria-controls="menu-panel">
  {t('menu')}
</button>
<div id="menu-panel" hidden={!open}>...</div>

aria-required-children / aria-required-parent

Composite roles need their child roles, and child roles need the right parent. Prefer semantic HTML (`<ul><li>`, `<select><option>`) over recreating these structures with ARIA.

// BAD, role="listbox" with no role="option" children
<div role="listbox">
  <div>{t('one')}</div>
  <div>{t('two')}</div>
</div>

// GOOD, semantic <select> with <option> children
<select aria-label={t('choose')}>
  <option value="1">{t('one')}</option>
  <option value="2">{t('two')}</option>
</select>

aria-valid-attr-value

`aria-*` attrs that take id refs must point at existing ids; boolean attrs take `true`/`false`, not arbitrary strings.

// BAD, aria-labelledby points at id that does not exist
<input aria-labelledby="missing-id" />

// GOOD, id exists in the DOM
<>
  <span id="email-label">{t('email')}</span>
  <input aria-labelledby="email-label" />
</>

focus-trap (focus management)

Modals must trap foc

Read more
Ships withgaia-react-gaia

Claude is raw power. GAIA is order and focus. The foundation that keeps Claude-shipped code production-grade as your team scales. The React frontend is handled. You build the rest of your app on top. Every convention enforced in code.

Get the whole plugin

Other skills on gaia-react-gaia.