Accessibility Rules for JSX
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.
The skill file
Cursor · .cursor/rules/*.mdc.cursor/rules/*.mdc3800 chars
---
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).
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursor/rules/accessibility-jsx-rules.mdc
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…