Coding Rules for building highly accessible web applications that conform to WCAG 2.2/3.0 draft, using semantic HTML, modern CSS, TypeScript/React, and automated + manual auditing.
Modern web development has an accessibility problem. 71% of websites fail basic WCAG compliance, not because developers don't care, but because accessibility is treated as an afterthought. You're shipping features that exclude millions of users while opening your organization to legal liability.
Every inaccessible component you ship creates technical debt that compounds:
The real problem? Most developers learn accessibility reactively—after an audit fails or a lawsuit arrives. By then, you're rewriting entire component libraries.
This Cursor Rules configuration transforms accessibility from compliance checkbox to development practice. Instead of bolting on ARIA attributes after the fact, you'll build semantically correct, inherently accessible interfaces from day one.
What This Ruleset Delivers:
Automated linting with eslint-plugin-jsx-a11y and jest-axe catches violations at development time, not during expensive QA cycles or post-launch audits.
// ❌ Automatically caught and blocked
<div onClick={handleClick}>Click me</div>
// ✅ Automatically suggested
<button onClick={handleClick}>Click me</button>
Built-in patterns ensure your components meet contrast ratios, keyboard navigation, and screen reader requirements without manual verification.
/* Automatically enforced contrast ratios */
.button-primary {
background: #0066cc; /* 4.7:1 ratio - passes AA */
color: #ffffff;
}
/* Built-in focus management */
.interactive:focus-visible {
outline: 2px solid #005ce6; /* 3.2:1 contrast - compliant */
outline-offset: 2px;
}
Semantic HTML reduces JavaScript payload while improving assistive technology performance. Your apps become faster AND more accessible.
// Developer writes initial component
const Modal = ({ children, onClose }) => (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content">
<span className="close-x" onClick={onClose}>×</span>
{children}
</div>
</div>
);
// QA finds keyboard users can't close modal
// Screen readers don't announce modal properly
// Focus isn't trapped
// Background content still accessible
// Developer spends 4+ hours retrofitting:
// - Adding ARIA attributes
// - Implementing focus trap
// - Adding keyboard handlers
// - Testing across screen readers
// Developer uses ruleset-guided patterns from start
const Modal = ({ children, onClose, title }) => (
<dialog
className="modal"
aria-labelledby="modal-title"
aria-modal="true"
onClose={onClose}
>
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
<button
className="modal-close"
onClick={onClose}
aria-label="Close dialog"
>
<CloseIcon aria-hidden="true" />
</button>
{children}
</div>
</dialog>
);
// Works correctly on first implementation:
// ✓ Keyboard accessible (Esc to close)
// ✓ Screen reader compatible
// ✓ Focus management automatic
// ✓ Background properly inert
Time Saved: 4+ hours per component × dozens of components = weeks of development time reclaimed.
// Ruleset-guided error handling
const LoginForm = () => {
const [errors, setErrors] = useState<Record<string, string>>({});
return (
<form onSubmit={handleSubmit} noValidate>
<div className="field-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
aria-describedby={errors.email ? "email-error" : undefined}
aria-invalid={!!errors.email}
required
/>
{errors.email && (
<div id="email-error" role="alert" className="error-message">
{errors.email}
</div>
)}
</div>
</form>
);
};
Result: Form errors are immediately announced to screen readers, keyboard navigation flows logically, and validation state is programmatically accessible.
npm install --save-dev eslint-plugin-jsx-a11y @axe-core/react jest-axe
Add to your .cursorrules file in your project root, then configure your build pipeline:
// package.json scripts
{
"scripts": {
"a11y:audit": "lighthouse-ci http://localhost:3000 --preset accessibility",
"test:a11y": "jest --testPathPattern=a11y.test",
"lint:a11y": "eslint --ext .tsx,.ts src/ --rule 'jsx-a11y/*: error'"
}
}
// src/components/Button/a11y.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from './Button';
expect.extend(toHaveNoViolations);
test('Button component is accessible', async () => {
const { container } = render(
<Button onClick={() => {}}>Click me</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
# .github/workflows/accessibility.yml
name: Accessibility Audit
on: [push, pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run accessibility tests
run: npm run test:a11y
- name: Lighthouse CI
run: npm run a11y:audit
// .vscode/settings.json
{
"editor.accessibilitySupport": "on",
"eslint.rules.customizations": [
{ "rule": "jsx-a11y/*", "severity": "error" }
]
}
Companies using accessibility-first development report:
Your next feature ship should be accessible by default, not by accident. This ruleset makes that automatic.
Start building inclusive experiences that work for everyone, perform better, and scale without accessibility debt. Your users—and your legal team—will thank you.
You are an expert in Accessible Web Development with HTML5 + CSS3, TypeScript, React 18, WAI-ARIA 1.2, and the latest WCAG 2.2 (watch-list: WCAG 3.0 drafts).
Key Principles
- Accessibility (A11y) is a first-class, non-negotiable requirement, not an optional enhancement.
- Follow progressive enhancement: build the core experience with semantic HTML; layer CSS and JS only when they do not break base accessibility.
- Design, build, and test against WCAG 2.2 AA today; track new success criteria from WCAG 3.0 for forward-compatibility.
- Content must be perceivable, operable, understandable, and robust (POUR). Verify every user story against these four pillars.
- Automate early, audit often, and include users with disabilities in usability testing.
- Fail fast: break builds on critical accessibility violations detected by CI tools (axe-linter, Lighthouse-CI).
HTML
- Always start with a valid <!doctype html>. Use lang="en" (or appropriate) on <html>.
- Use semantic landmarks: <header>, <nav>, <main>, <aside>, <footer>. Do NOT nest <main>.
- Headings (<h1>–<h6>) must form a strictly descending, hierarchical outline. Never skip a level.
- Alt text: images require meaningful alt unless decorative, then alt="" and role="presentation".
- Use <button>, <a>, <input type="…"> instead of <div> or <span> with click handlers.
- Use table markup only for tabular data. Provide <caption>, <thead>, <th scope="…">.
- Provide ARIA only if native semantics cannot achieve the goal. Prefer role="…" over aria-xyz attributes when both are valid.
CSS
- Meet WCAG 2.2 contrast ratio: 4.5:1 normal text, 3:1 large text/graphics.
- Never lock font-size in px for body text. Use rem/em and respect user zoom settings.
- Do not disable outline. Replace default focus styles only with AA-complaint custom :focus-visible styles (≥3:1 contrast).
- Ensure content reflows at 320 CSS px without loss of information (WCAG 1.4.10).
TypeScript / JavaScript
- No DOM-manipulating side effects before DOMContentLoaded.
- Keyboard first: every interactive component must expose key bindings identical to native widgets (e.g., Space/Enter on buttons, Arrow keys in menus).
- Use addEventListener with { once: false, passive: true } for scroll/touch to preserve performance.
- ARIA live regions: aria-live="assertive" only for critical, time-sensitive updates; else "polite".
- Avoid setting tabindex"-1" on focusable elements unless using focus management (e.g., dialog traps). Never set tabindex>0; rely on document order.
- Implement all custom widgets per ARIA-Authoring-Practices specification.
Error Handling and Validation
- Validate user input both client- and server-side; expose errors programmatically:
• role="alert" or aria-live="assertive" so screen readers announce.
• Link error summary to fields via aria-describedby.
- Provide clear, plain-language error messages: describe the problem + how to solve it.
- Use try/catch around async operations; surface errors with toast components that include aria-live and can be dismissed via Esc key.
- Use early returns for invalid state; keep the happy path last for comprehension.
React 18 (Framework-Specific Rules)
- Prefer functional components + hooks; TypeScript interfaces for props.
- Wrap app in <ErrorBoundary⟩ with fallback UIs that meet WCAG (focus management, described errors).
- Use React-Aria or Reach-UI primitives instead of building from scratch when possible.
- Manage focus on route changes: move focus to <h1> or first interactive element.
- For dialogs: use <dialog> where supported, else aria-modal="true", role="dialog", focus trap, inert background; restore focus on close.
- Leverage React-Testing-Library + jest-axe for unit tests; block merge on high-impact violations.
Additional Sections
Testing
- Automated: integrate axe DevTools, Lighthouse-CI, and jest-axe in GitHub Actions.
- Manual: check with keyboard only, screen reader (NVDA + VoiceOver), high contrast mode, 200% zoom, prefer-reduced-motion.
- Involve users with disabilities at feature-complete milestones. Budget at least one test cycle per sprint.
Performance
- Keep first contentful paint <1.8 s. Poor performance disproportionately harms assistive tech users.
- Avoid layout-thrashing animations; respect prefers-reduced-motion.
- Lazy-load non-critical images with native loading="lazy".
Security
- Accessible auth: support password managers, avoid CAPTCHA without accessible alternative (e.g., hCaptcha + audio challenge).
- Error pages must return correct HTTP status, offer contact info, and provide a “Back to home” link.
Content & Documentation
- Write in plain English, ≤160 characters per sentence.
- Provide glossary for acronyms; first occurrence: <abbr title="World Health Organization">WHO</abbr>.
- Document keyboard shortcuts and ARIA patterns in a public A11y MDX file.
File/Folder Conventions
- src/components/<name>/<name>.tsx – component
- src/components/<name>/a11y.test.tsx – jest-axe tests
- styles/modules/ – SCSS modules, kebab-case
- Static media: public/img, each image accompanied by image-name.alt.txt if descriptive text is long.
Tooling
- Pre-commit: lint-staged runs eslint-plugin-jsx-a11y & stylelint-a11y.
- vscode-settings.json adds "editor.accessibilitySupport": "on".
- npm scripts: "a11y:audit": "lighthouse-ci http://localhost:3000 --preset accessibility".
Continuous Improvement
- Track WCAG draft updates quarterly; log gaps and create backlog tickets.
- Run post-launch audits monthly via Siteimprove or AudioEye; critical issues triaged within 24 h.