Skip to content

Add CodeAI accessibility agent + skill (a11y-architect)#73329

Open
stephenliang wants to merge 2 commits into
stagingfrom
stephen/a11y-agent
Open

Add CodeAI accessibility agent + skill (a11y-architect)#73329
stephenliang wants to merge 2 commits into
stagingfrom
stephen/a11y-agent

Conversation

@stephenliang

@stephenliang stephenliang commented Jun 17, 2026

Copy link
Copy Markdown
Member

Adds a CodeAI-specific accessibility agent and skill, usable by both Claude and Codex, for building and auditing UI to WCAG 2.2 Level AA (the floor, not the target). It is based on the generic ECC a11y-architect agent and accessibility skill, re-grounded in CodeAI's own practice: the design-system-first component hierarchy, the apps/ (legacy) vs frontend/ (new code, strict jsx-a11y) regimes, our focus-ring conventions and tokens, and the accessibility constraints of our Blockly-based labs. This is documentation only — no runtime code is touched.

Artifacts

flowchart TD
  agent["a11y-architect agent<br/>.claude/agents"] --> skill["accessibility SKILL.md<br/>.agents/skills/accessibility"]
  skill --> checklist["checklist.md<br/>(org ground truth)"]
  skill --> blockly["blockly.md<br/>(Blockly-lab a11y)"]
  skill --> ds["design-system skill"]
Loading

The skill is the knowledge base and the agent is a thin workflow persona that defers to it; the checklist is the org's ground truth and wins whenever it disagrees with the skill.

  • .agents/skills/accessibility/SKILL.md — the skill, and the source of truth for everything else. It is cross-tool: both Claude and Codex discover .agents/skills (.claude/skills symlinks to it). It mirrors the ECC accessibility skill's POUR structure and verbiage, with CodeAI specifics woven directly into the How-It-Works steps rather than bolted on as a separate section.
  • .agents/skills/accessibility/checklist.md — the canonical org accessibility build checklist and keyboard/screen-reader self-test steps; the skill's declared ground truth.
  • .agents/skills/accessibility/blockly.md — a Blockly-lab accessibility reference, forked out because its rules differ sharply: two separate Blockly stacks, no screen-reader support in either, and theming (high-contrast and colorblind palettes) as the only shipped accessibility win.
  • .claude/agents/a11y-architect.md — a repo-local Claude subagent that reads the skill as its rubric and routes work through the design system; the generic ECC prompt-defense baseline is stripped.

Example: agent test runs

Two end-to-end runs on real apps/ components, showing the agent's judgment in both directions — convert what should be a control, and leave alone what only looks like one. Both were throwaway demonstrations and are not part of this PR.

1. A true div-button → a justified native control (StudentRubricView.tsx)

A Lab2 rubric panel that already uses the design system (MUI Typography, @code-dot-org/component-library icons). Its evidence-level header was a hand-rolled <div onClick={() => setCollapsed(...)}> disclosure toggle: keyboard-unreachable, no role, no accessible name, collapse state conveyed by color alone. The agent swept for interactive behavior on non-interactive elements, made an explicit component decision before writing code (no DSCO disclosure/accordion primitive fits an inline icon+label toggle; an MUI Accordion would restructure the card), then chose a native <button aria-expanded aria-controls> and recorded the rationale as a code comment. It also named the icon-only goal-switcher buttons, gave the loading spinner a role="status", and hid the decorative ProgressRing SVG.

-      <div className={styles.header} onClick={() => setCollapsed(!collapsed)}>
+      {/* Component decision: native <button> — no design-system disclosure/accordion
+          primitive fits an inline icon+label toggle; MUI Accordion would restructure the
+          card. A native button gains keyboard focus, Enter/Space, aria-expanded + aria-controls. */}
+      <button
+        type="button"
+        className={styles.headerButton}
+        onClick={() => setCollapsed(!collapsed)}
+        aria-expanded={!collapsed}
+        aria-controls={panelId}
+      >
         ...
-      </div>
+      </button>

WCAG 2.2 addressed across the file: 2.1.1 (Keyboard), 4.1.2 (Name/Role/Value), 4.1.3 (Status Messages), 1.1.1 (Non-text Content), 1.4.1 (Use of Color), 2.4.11 (Focus Appearance).

2. Restraint: a clickable wrapper correctly left alone (LtiSectionSyncDialog.tsx)

A late-2023 multi-view LTI roster-sync dialog. The sweep flagged a <div onClick={handleDocsClick}>, but the agent reasoned it is an analytics-capture wrapper around SafeMarkdown's real <a> links — already keyboard-reachable — not a div-button, so it left the element in place and documented why instead of wrapping it in a redundant control. In the same pass it fixed the genuine barriers, e.g. dropping a bogus role="grid" and giving the summary table a <caption> and scope="col" headers:

           <h2 style={styles.dialogHeader} id={HEADING_ID_SYNC_RESULT}>
             {dialogTitle}
           </h2>
+          {/*
+           * The onClick here fires analytics when the user clicks any link
+           * inside the description markdown. It is intentionally on a non-
+           * interactive wrapper; the actual interactive elements (links) are
+           * rendered by SafeMarkdown and remain keyboard-operable.
+           */}
           <div onClick={handleDocsClick}>
             <SafeMarkdown openExternalLinksInNewTab markdown={dialogDescription} />
           </div>
-          <div style={styles.summaryContainer} aria-labelledby={'roster-sync-status'}>
+          <div style={styles.summaryContainer}>
@@ summary table @@
-      <table style={styles.summaryTable} role={'grid'}>
+      <table style={styles.summaryTable}>
+        <caption style={styles.visuallyHidden}>
+          {i18n.ltiSectionSyncDialogTitle()}
+        </caption>
         <thead>
           <tr>
-            <th style={styles.tableHeaderLeft}>
+            <th style={styles.tableHeaderLeft} scope="col">
               {i18n.ltiSectionSyncDialogHeaderSectionName()}
             </th>
             {/* …the other three headers likewise gain scope="col"… */}

It also added a role="dialog" aria-modal aria-labelledby wrapper, a spinner role="status" live region, made duplicate "Primary Instructor" dropdown labels unique per section, and bumped a 2.83:1 header color to a 4.5:1-passing token.

Both runs verified with ./tools/hooks/pre-commit (clean), used only ./tools/hooks/pre-commit for verification (no yarn from the worktree), and flagged what they could not self-verify — keyboard and screen-reader passes, forced-colors, and prefers-reduced-motion — rather than claiming them.

Links

  • Jira: N/A

Testing story

Documentation only; no runtime code, so no automated suite applies. The content was verified by a factual-claim audit against the live repo — confirming the disabled jsx-a11y rules in apps/.eslintrc.js, the @axe-core/playwright dependency, the mixins.focus-styles mixin and --borders-brand-teal-primary token, the design-system skill, every referenced Blockly path, the intra-skill relative links, and the agent frontmatter — plus an internal-consistency and CLAUDE.md/AGENTS.md conventions review across the four files. The two agent test runs above are a further end-to-end check that the agent + skill produce house-style output. ./tools/hooks/pre-commit lints only changed files of the relevant types, and these are all markdown, so no js/ts/ruby lint applies.

stephenliang and others added 2 commits June 17, 2026 15:24
Add a CodeAI-specific accessibility agent and skill (usable by Claude and Codex), based on the generic ECC a11y-architect agent and accessibility skill, re-grounded in CodeAI practice: design-system-first components, the apps/ vs frontend/ regimes, focus-ring conventions, and Blockly's constraints. Documentation only; no runtime code.

- .agents/skills/accessibility/SKILL.md: the skill (cross-tool; both Claude and Codex discover .agents/skills)
- .agents/skills/accessibility/checklist.md: canonical org build checklist (ground truth)
- .agents/skills/accessibility/blockly.md: Blockly-lab accessibility reference
- .claude/agents/a11y-architect.md: repo-local Claude subagent that defers to the skill

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…runs

- skill Step 1 leads with the design-system-first priority order (component > justified raw native element > never a div/span widget)
- add a div-button sweep (onClick/onKeyDown/role on div/span/p) so audits stop missing keyboard-inaccessible controls
- agent: require a per-interactive-element component decision in the output
- agent: drop yarn typecheck/test/build from worktree verification (no node_modules; sandbox blocks copy-out) in favor of ./tools/hooks/pre-commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@stephenliang stephenliang marked this pull request as ready for review June 17, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant