Accessibility Rules for JSX
Cursor.cursor/rules/*.mdcCoding
A11y rules putting semantic HTML before ARIA, with concrete requirements for focus management, labels, and color contrast.
Use when: Attach to TSX files in any React codebase to catch div-button soup, focus traps, and ARIA misuse while components are being written.
Get this skill
Add the skill file with the CLI, by hand, or over MCP.Save this to your project or home directory so Cursor can load it.
npx goodprompts add accessibility-jsx-rulesSaves to
./.cursor/rules/accessibility-jsx-rules.mdcnpx goodprompts add accessibility-jsx-rulesSaves to
./.cursor/rules/accessibility-jsx-rules.mdcSkill file
.cursor/rules/*.mdc3,800 characters---
description: Accessibility requirements for React/JSX components
globs: "**/*.tsx"
alwaysApply: false
---
# Accessibility rules
## Semantic elements first
- Interactive things use interactive elements: `<button>` for actions, `<a href>` for navigation. A `<div onClick>` is a defect — it has no keyboard activation, no focus, no role. If a design demands a clickable card, wrap or overlay a real `<button>`/`<a>` inside it.
- Choose the element by behavior: navigates to a URL → `<a>`; submits or triggers → `<button type="button">` (always set `type` — the default `submit` causes ghost form submissions).
- Structure with landmarks: one `<main>` per page, `<nav>` for nav, `<header>`/`<footer>`, `<section>` only with an accessible name. Heading levels descend without skipping; never pick a heading tag for its font size — style it instead.
- Lists of things are `<ul>/<li>`; data grids are `<table>` with `<th scope>`. Recreating either out of divs removes structure screen readers navigate by.
## ARIA only as a last resort
- The order of preference is: native element → native attribute → ARIA. `aria-disabled` on a div-button when `<button disabled>` exists is wrong twice.
- No redundant roles: `role="button"` on `<button>`, `role="navigation"` on `<nav>` — delete them.
- Acceptable ARIA in this codebase: `aria-label` on icon-only buttons, `aria-expanded`/`aria-controls` on disclosure triggers, `aria-current="page"` on active nav links, `aria-live="polite"` regions for async status, `aria-describedby` linking inputs to error text.
- If you reach for `role="dialog"`, `role="listbox"`, `role="tablist"`, or similar composite widgets, stop — use the project's existing primitives (Radix/shadcn/react-aria) instead of hand-implementing the keyboard contract.
- Decorative images/icons: `alt=""` or `aria-hidden="true"`. Informative images: `alt` describing meaning, not appearance ("Delete invoice", not "trash icon").
## Focus management
- Every interactive element shows a visible focus ring via `focus-visible:` styles. Never `outline-none` without an equal-or-better replacement in the same class list.
- Dialogs/drawers/popovers: focus moves into the surface on open, is trapped while open, Escape closes, and focus returns to the trigger on close. With Radix/shadcn primitives this is built in — do not re-implement, and do not break it with `autoFocus` on arbitrary children.
- After client-side route changes, move focus to the new page's `<h1>` (tabIndex={-1}) or skip-link target — otherwise keyboard users are stranded at the old position.
- After destructive actions that remove the focused element (delete row), explicitly move focus to a sensible neighbor or the list container.
- DOM order = visual order. No positive `tabIndex`, ever; `tabIndex={-1}` only for programmatic focus targets. Do not reorder visually with CSS (`order`, `flex-row-reverse`) in ways that contradict tab order.
## Forms and labels
- Every input has a `<label htmlFor>` (or wrapping label). Placeholder is never the only label — it vanishes on input.
- Errors: set `aria-invalid` on the field, render the message in an element referenced by `aria-describedby`, and move focus to the first invalid field on failed submit.
- Group related radios/checkboxes in `<fieldset>` with a `<legend>`.
## Contrast and content
- Text contrast ≥ 4.5:1 (3:1 for ≥ 24px text); UI control boundaries and focus indicators ≥ 3:1. Use the design tokens — `text-muted-foreground` on `bg-background` passes; one-off opacity tweaks (`text-foreground/40`) usually fail and need checking.
- Never encode meaning in color alone: pair status colors with an icon or text ("Failed", not just red).
- Respect `prefers-reduced-motion`: gate non-essential animation behind the media query (`motion-safe:` variants).