rule-accessibility
MANDATORY when editing files matching ["frontend/src/**/*.tsx", "frontend/src/**/*.ts"]. Accessibility requirements for all frontend code. WCAG 2.1 AA and Section 508 compliance is legally mandated for this federal project.
Install
mkdir -p .claude/skills/rule-accessibility && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14466" && unzip -o skill.zip -d .claude/skills/rule-accessibility && rm skill.zipInstalls to .claude/skills/rule-accessibility
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
MANDATORY when editing files matching ["frontend/src/**/*.tsx", "frontend/src/**/*.ts"]. Accessibility requirements for all frontend code. WCAG 2.1 AA and Section 508 compliance is legally mandated for this federal project.About this skill
Accessibility Rules (WCAG 2.1 AA / Section 508)
This is a federal government project. Section 508 compliance and WCAG 2.1 AA conformance are legal requirements, not optional best practices. Accessibility violations are treated as bugs, not suggestions.
Jest-axe Testing (Required)
EVERY component test suite MUST include a jest-axe accessibility scan. MUST use the toHaveNoViolations() matcher. NEVER ship a component without an axe scan in its test file.
Example from codebase:
// From frontend/tests/components/manageUsers/InviteLegacyUsersButton.test.tsx
import { axe } from "jest-axe";
it("should not have accessibility violations", async () => {
const { container } = render(
<InviteLegacyUsersButton organizationId="org-123" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
ARIA Labels and Roles
EVERY interactive element MUST have a proper ARIA label or accessible name. Icon-only buttons MUST have aria-label describing the action. NEVER rely on visual appearance alone to convey meaning.
Example from codebase:
// Correct: icon-only button with aria-label
<button aria-label="Close modal" onClick={onClose}>
<Icon name="close" />
</button>
// Correct: form input with associated label
<label htmlFor="search-input">Search grants</label>
<input id="search-input" type="text" />
Keyboard Navigation
ALWAYS maintain logical tab order. ALWAYS ensure all interactive elements are reachable via keyboard. NEVER use tabIndex values greater than 0 (disrupts natural tab order). ALWAYS use tabIndex={0} for custom interactive elements and tabIndex={-1} for programmatically focusable non-interactive elements.
Focus Management
ALWAYS use focus-trap-react for modals, dialogs, and drawers. ALWAYS return focus to the trigger element when a modal/dialog closes. NEVER trap focus in non-modal UI elements.
Example from codebase:
// From frontend/src/components/ modal pattern
import FocusTrap from "focus-trap-react";
<FocusTrap active={isOpen}>
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Confirm Action</h2>
{/* Modal content */}
<button onClick={onClose}>Close</button>
</div>
</FocusTrap>
Heading Hierarchy
ALWAYS maintain logical heading hierarchy (h1 > h2 > h3). NEVER skip heading levels. EVERY page MUST have exactly one <h1>. NEVER use headings for visual styling — use CSS classes instead.
Form Accessibility
EVERY form input MUST be associated with a <label> element (via htmlFor/id or wrapping). ALWAYS use aria-describedby for help text and error messages. ALWAYS use aria-invalid="true" for fields with validation errors. NEVER use placeholder text as a substitute for labels.
Example from codebase:
<label htmlFor="email-input">Email address</label>
<input
id="email-input"
type="email"
aria-describedby="email-help email-error"
aria-invalid={hasError}
/>
<span id="email-help">We will never share your email.</span>
{hasError && <span id="email-error" role="alert">Please enter a valid email.</span>}
Dynamic Content and Live Regions
ALWAYS use aria-live regions for dynamic content updates (loading states, error messages, notifications). ALWAYS use role="alert" for urgent error messages. ALWAYS use role="status" for non-urgent status updates. NEVER update DOM content without notifying assistive technology.
Example from codebase:
// Loading state announced to screen readers
<div aria-live="polite" aria-busy={isLoading}>
{isLoading ? "Loading results..." : `${count} results found`}
</div>
// Error announced immediately
<div role="alert">
{errorMessage}
</div>
Color and Visual Design
NEVER convey information solely through color. ALWAYS provide text or icons alongside color indicators. ALWAYS ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text per WCAG AA). ALWAYS use USWDS design tokens for colors — they meet contrast requirements by default.
Images and Icons
ALWAYS provide meaningful alt text for informational images. ALWAYS use aria-hidden="true" or alt="" for decorative images and icons. NEVER leave alt attributes empty on informational images.
USWDS Component Usage
ALWAYS prefer USWDS components from @trussworks/react-uswds for standard UI elements — they include built-in accessibility. NEVER reimplement functionality that USWDS provides with accessible defaults. ALWAYS verify that custom components meet or exceed USWDS accessibility standards.
Pa11y-CI Integration
Be aware that pa11y-ci runs in CI on every PR with two configurations:
- Desktop: Standard viewport with axe runner
- Mobile: 390x844 viewport (iPhone 14 equivalent)
Both wait for
#main-contentvisibility before scanning. ALWAYS ensure pages render#main-contentas a landmark.
Tables
ALWAYS use semantic <table>, <thead>, <tbody>, <th>, <td> elements for tabular data. ALWAYS use scope="col" or scope="row" on <th> elements. NEVER use tables for layout purposes. ALWAYS provide a <caption> or aria-label for data tables.
Skip Navigation
ALWAYS ensure a "Skip to main content" link exists as the first focusable element on the page. This is handled at the layout level — NEVER remove it.
Context Enrichment
When generating frontend code that affects accessibility, enrich your context:
- Call
get_architecture_section("frontend")from thesimpler-grants-contextMCP server to understand frontend architecture - Call
get_rule_detail("frontend-components")for component structure patterns - Call
get_rule_detail("frontend-tests")for jest-axe test patterns - Call
get_rule_detail("frontend-app-pages")for page-level accessibility patterns - Consult Compound Knowledge for indexed documentation on accessibility patterns
Related Rules
When working on accessibility, also consult these related rules:
frontend-components.mdc— component structure, USWDS usagefrontend-tests.mdc— jest-axe testing requirementsfrontend-e2e-tests.mdc— E2E accessibility testingfrontend-app-pages.mdc— page-level metadata and structurefrontend-i18n.mdc— translated strings for accessible labelscross-domain.mdc— cross-cutting conventions
Specialist Validation
When generating or significantly modifying frontend code:
For simple changes (< 20 lines, adding aria-label, fixing alt text): No specialist invocation needed — the directives in this rule file are sufficient.
For moderate changes (new interactive component, new form, new modal):
Invoke accessibility-auditor to validate WCAG 2.1 AA compliance.
For complex changes (new page, new layout, new navigation pattern): Invoke the following specialists (run in parallel where possible):
accessibility-auditor— deep WCAG 2.1 AA / Section 508 compliance reviewcodebase-conventions-reviewer— validate against project conventionskieran-typescript-reviewer— TypeScript-specific quality review