stage 6 · stewardshipstewardshipauditaccessibilitywcag

Audit Components Against the Accessibility Floor

Produces WCAG 2.2 and APCA findings for your current token set, with specific remediation recommendations.

play · accessibility-audit
Use this play to evaluate the current state of the design system's components against the non-negotiable accessibility requirements. Produces a structured report with severity levels. **Step 1 — Read the living brief:** Read `LIVING_BRIEF.md`. Note which components are implemented and any accessibility decisions already recorded. **Step 2 — Read the references:** Read the following from the Sistema knowledge base: - Accessibility floor: `{{sistema_url}}/raw/principles/accessibility/floor` This is the normative checklist. Every item marked "must" in that document is a blocking violation if not met. **Step 3 — Define the audit scope:** Components to audit: [List the components you want to audit, or write "all implemented components" to audit everything in the living brief's current state list.] **Step 4 — Evaluate each component:** For each component, evaluate against each section of the accessibility floor: 1. **Color contrast** — Do all text/background pairings meet 4.5:1 (normal text) or 3:1 (large text / UI components)? 2. **Keyboard navigation** — Is every interactive element reachable by keyboard? Can it be activated with the correct key? 3. **Focus visibility** — Is the focus indicator visible? Does it meet 3:1 contrast against the adjacent surface? 4. **Touch targets** — Is the interactive area ≥ 44×44px? 5. **Semantic structure** — Does the component use semantic HTML or a full ARIA implementation? 6. **Accessible name** — Does every interactive element have an accessible name? 7. **Dynamic state** — Are state changes (expanded, selected, checked) exposed via ARIA attributes? **Step 5 — Produce the audit report:** For each component: - Pass / Fail / Needs verification per criterion - Specific violations with severity: **blocking** (WCAG A/AA violation), **recommended** (best practice not met), **minor** (enhancement) Summary: - Components fully passing - Components with blocking violations (must fix before ship) - Components with recommended improvements ---
1 KB ref
paste intoClaude CodeCursor
Example outputwhat a successful run looks like
accessibility-audit · example output
Example output
About this example: Generated for "Verdant" post-sprint audit. Demonstrates a realistic accessibility audit across all seven implemented components. Three blocking violations are found: a DataTable sort button lacking an accessible name, a Toast error state failing 4.5:1 contrast against the surface, and a Badge touch target below 44px. Non-obvious detail: the Modal overlay focus trap is flagged as "needs verification" rather than pass or fail, because correctness depends on the focus management library used — this reflects a real edge case in how audits handle framework-dependent behavior.

Step 1 — Living brief context

Token foundation: Complete. Semantic color tokens: --color-primary (#1A6B3A), --color-error (#D62B2B), --color-on-surface, --color-surface, --color-primary-container. Light + dark mode both required.

Implemented components: Button (primary/secondary/ghost), Input (text/password), Badge (status), DataTable (sortable), Card, Modal, Toast.

Accessibility decisions already recorded:

  • prefers-reduced-motion implemented globally
  • Focus ring: 2px solid var(--color-primary) with 2px offset (recorded in living brief)

Step 2 — Reference loaded

Accessibility floor synthesis loaded. Non-negotiable requirements confirmed:

  • Color contrast: 4.5:1 for normal text, 3:1 for large text and UI components
  • Keyboard: all interactive elements reachable and activatable
  • Focus indicator: visible, meets 3:1 contrast against adjacent surface
  • Touch targets: ≥ 44×44px
  • Semantic HTML or full ARIA implementation
  • Accessible name on every interactive element
  • ARIA state attributes for dynamic components

Step 3 — Audit scope

All implemented components: Button, Input, Badge, DataTable, Card, Modal, Toast.


Step 4 — Evaluation per component

Button

CriterionResult
Color contrastPass — primary (#1A6B3A) on white: 7.2:1; ghost text on white: 6.1:1
Keyboard navigationPass — native <button>, receives Tab focus, activates on Enter/Space
Focus visibilityPass — 2px ring at 3px offset, confirmed 4.8:1 contrast against white surface
Touch targetsPass — min-height: 44px enforced in CSS
Semantic structurePass — native <button> element throughout
Accessible namePass — text content used as label in all three variants
Dynamic statePass — disabled state uses disabled attribute, not just visual

Result: Fully passing


Input

CriterionResult
Color contrastPass — placeholder (#9CA3AF) on white: 2.8:1 (decorative, exempt); label + value: 7.1:1
Keyboard navigationPass — native <input>
Focus visibilityPass — matches Button focus ring
Touch targetsPass — input height 44px
Semantic structurePass — <label> element, associated via htmlFor/id
Accessible namePass — label visible and programmatically associated
Dynamic statePass — aria-invalid applied on validation error

Result: Fully passing


Badge

CriterionResult
Color contrastPass — status text meets 4.5:1 in all variants
Keyboard navigationPass — non-interactive; no Tab stop required
Focus visibilityN/A — non-interactive
Touch targetsFail — badge renders at 24×20px; no interactive affordance but frequently used in table rows where the entire row is a link target — the badge itself is below 44px
Semantic structurePass — uses <span> with no misleading role
Accessible nameN/A — decorative; text content is the label
Dynamic stateFail — status changes update visually but no aria-live region announces changes to screen readers

Result: 2 violations (see below)


DataTable

CriterionResult
Color contrastPass — header text 7.1:1; body text 6.8:1; hovered row background meets 3:1 for UI component
Keyboard navigationPass — sort buttons receive Tab focus; sort activates on Enter/Space
Focus visibilityPass — sort button focus ring matches system standard
Touch targetsPass — sort button click area ≥ 44px height via padding
Semantic structurePass — <table>, <thead>, <th scope="col"> throughout
Accessible nameFail — sort buttons (<th onClick={handleSort}>) contain only the column label text, no aria-sort attribute; screen readers cannot determine current sort direction
Dynamic stateFail — same issue as above: no aria-sort="ascending" / aria-sort="descending" on sorted column headers

Result: 2 violations (see below)


Card

CriterionResult
Color contrastPass — all text on card surface meets 4.5:1
Keyboard navigationPass — non-interactive wrapper; interactive children handle their own focus
Focus visibilityN/A — non-interactive
Touch targetsN/A — non-interactive
Semantic structurePass — renders as <article> or <section> depending on context
Accessible nameNeeds verification — depends on consumer usage; no built-in heading requirement enforced
Dynamic stateN/A

Result: Needs verification (accessible name — consumer responsibility)


Modal

CriterionResult
Color contrastPass — modal surface is white; all text meets 4.5:1
Keyboard navigationPass — Tab key navigates within modal; Escape closes
Focus visibilityPass — focus returns to trigger on close
Touch targetsPass — close button ≥ 44×44px
Semantic structurePass — role="dialog", aria-modal="true"
Accessible namePass — aria-labelledby pointing to modal heading
Dynamic stateNeeds verification — focus trap correctness depends on implementation; if using a focus trap library, verify it handles dynamically added focusable elements. Cannot confirm pass/fail without runtime inspection.

Result: 1 needs verification (focus trap)


Toast

CriterionResult
Color contrastFail — error Toast uses hardcoded #D62B2B background with white text; measured contrast: 4.1:1 (below 4.5:1 threshold for normal text)
Keyboard navigationPass — dismiss button receives focus; activates on Enter/Space
Focus visibilityPass — dismiss button focus ring meets standard
Touch targetsPass — dismiss button ≥ 44×44px
Semantic structurePass — role="status" for success/info; role="alert" for error
Accessible namePass — text content provides label
Dynamic statePass — aria-live region used for announcements

Result: 1 violation (see below)


Step 5 — Audit report

Summary

SeverityCount
Blocking4
Recommended1
Needs verification2

Blocking violations

1. DataTable — sort column headers missing aria-sort

Sort buttons do not expose sort direction to assistive technology. Screen reader users cannot determine which column is sorted or in which direction.

  • Fix: Add aria-sort="ascending" / aria-sort="descending" / aria-sort="none" to <th> elements that have sort behavior
  • Note: aria-sort goes on the <th>, not the <button> inside it

2. DataTable — sort buttons missing accessible name for direction

Related to above: even with aria-sort added, the button text alone ("Name", "Date") doesn't indicate it's a sort control. Consider adding aria-label="Sort by Name" or a visually hidden description.

  • Fix: Add aria-label or visually hidden text to each sort button: "Sort by [column]"

3. Toast — error background fails 4.5:1 contrast

Hardcoded #D62B2B with white text measures 4.1:1 — below the WCAG AA threshold for normal text.

  • Fix: Replace #D62B2B with var(--color-error) (this is also a token audit violation). Update --color-error to a value that meets 4.5:1 against white — e.g. #C0241F (4.6:1). Verify dark mode separately.
  • Note: This is both a token violation (audit-token-coverage) and an accessibility violation. Fix the token first; test contrast after.

4. Badge — no aria-live for dynamic status changes

When Badge status changes programmatically (e.g. "Processing" → "Complete"), the change is not announced to screen readers.

  • Fix: Wrap the Badge status text in an aria-live="polite" region, or ensure the parent component announces the state change via a live region
  • Scope: Only applies when Badge status is dynamic. Static badges (rendered once, never updated) are exempt.

Recommended improvements

5. Badge — touch target below 44px when used interactively

Badge currently renders at 24×20px. This is acceptable for a purely decorative element, but if Badge is ever wrapped in a link or button (common in table rows), the touch target will be below 44px.

  • Recommendation: Add a minimum padding or hit area to Badge when used interactively, or document in the living brief that Badge must not be the sole click target
  • Why recommended (not blocking): Badge is not currently interactive by itself; this is a pattern-use risk, not a component defect

Components fully passing

  • Button — all criteria pass
  • Input — all criteria pass
  • Card — pass (accessible name is consumer responsibility; document in pattern guidance)

Components with blocking violations (must fix before ship)

  • DataTable — 2 blocking violations (aria-sort, accessible button names)
  • Toast — 1 blocking violation (contrast failure on error state)
  • Badge — 1 blocking violation (aria-live for dynamic state)

Needs verification

  • Modal — focus trap correctness (verify with runtime screen reader test using NVDA or VoiceOver)
  • Card — accessible name in context (document consumer responsibility in pattern docs)

Recommended fix order

This sprint — blocking:

  1. Toast.tsx — swap hardcoded #D62B2B for var(--color-error); update --color-error value to meet 4.5:1 (combined fix with audit-token-coverage blocking item 3)
  2. DataTable.tsx — add aria-sort to <th> elements; add aria-label to sort buttons
  3. Badge — add aria-live="polite" wrapper when status is dynamic

Next sprint — recommended: 4. Badge — document interactive touch target guidance in living brief and pattern docs

Verify before release: 5. Modal — runtime focus trap test with VoiceOver (macOS) and NVDA (Windows)

In this play

Prompt body
Example output
References1

Part of a campaign

Audit and Refactor a Design System

Step 3 of 5.

Continue campaign

References pulled in

principles/accessibility/floor
Tip

Plays work best when your agent has read DESIGN.md first. Run session-start at the beginning of each session to orient it.