# How to Generate Accessible UI with Cursor

- Tool: Cursor
- Difficulty: Beginner
- Time required: 10 min
- Compatibility: Cursor Pro+, any project
- Last updated: March 2026

## TL;DR

How to generate accessible UI with Cursor. How to create .cursorrules that enforce ARIA attributes and accessibility. How to prompt Cursor to generate accessible React components. Configure .cursorrules and use @file context in Cursor Chat (Cmd+L) or Composer (Cmd+I) for best results.

## Why Cursor Skips Accessibility by Default

Cursor generates functional components but often omits ARIA attributes, keyboard navigation, focus management, and screen reader labels. Adding accessibility rules to your project ensures every generated component meets WCAG guidelines. This tutorial covers .cursorrules for a11y enforcement, prompting patterns for accessible UI, and post-generation auditing with axe-core.

## Before you start

- Cursor installed (Free or Pro)
- A React project with UI components
- Basic understanding of WCAG and ARIA attributes
- Optional: axe-core or eslint-plugin-jsx-a11y installed

## Step-by-step guide

### 1. Add accessibility rules to .cursorrules

Create rules that enforce ARIA attributes, keyboard navigation, and semantic HTML in all generated components. Specify that all interactive elements must be keyboard accessible and all images must have alt text.

```
# .cursorrules

## Accessibility Rules (WCAG 2.1 AA)
- All interactive elements must be keyboard accessible (tabIndex, onKeyDown)
- All images must have descriptive alt text (never alt="")
- Use semantic HTML: nav, main, section, article, aside, header, footer
- All form inputs must have associated labels (htmlFor + id)
- Use aria-label on icon-only buttons
- Use aria-live for dynamic content updates
- Color contrast must meet 4.5:1 minimum ratio
- Focus must be visible on all interactive elements
- Modals must trap focus and return focus on close
- Use role attributes only when semantic HTML is insufficient
```

> Pro tip: Add eslint-plugin-jsx-a11y to your project. Cursor can then use @Lint Errors to catch accessibility violations after generation.

**Expected result:** Cursor includes ARIA attributes and semantic HTML in all generated components.

### 2. Generate accessible components with Composer

When generating UI components, explicitly request accessibility features. Reference your existing accessible components with @file so Cursor follows the same patterns.

```
// Prompt to type in Cursor Composer (Cmd+I):
// @src/components/shared/AccessibleModal.tsx
// Generate a new Dropdown component with full accessibility:
// - Keyboard navigation (arrow keys, Enter, Escape)
// - aria-expanded, aria-haspopup, aria-activedescendant
// - Focus trap when open, return focus on close
// - Screen reader announcements for selection changes
// - Follow WAI-ARIA Combobox pattern
// Use the same structure as AccessibleModal.tsx.
```

> Pro tip: Reference the WAI-ARIA authoring practices with @Docs if you have indexed the W3C documentation.

**Expected result:** A fully accessible dropdown component with proper ARIA attributes and keyboard handling.

### 3. Audit generated components with Cmd+K

After generating a component, select it and use Cmd+K to ask Cursor to audit it for accessibility issues. Cursor will identify missing ARIA attributes, keyboard gaps, and semantic HTML improvements.

```
// Select a component in your editor, press Cmd+K:
// Audit this component for WCAG 2.1 AA accessibility issues.
// Check for: missing ARIA attributes, keyboard navigation gaps,
// semantic HTML improvements, color contrast issues, focus management.
// Fix all issues and explain each change.
```

**Expected result:** Cursor identifies and fixes accessibility issues in the selected component.

### 4. Create an auto-attaching a11y rule for components

Create a .cursor/rules/accessibility.mdc that auto-attaches for all component files, ensuring accessibility standards apply to every generated component.

```
---
description: Accessibility enforcement for UI components
globs: "src/components/**, src/pages/**, **/*.tsx"
alwaysApply: false
---

- Every button must have accessible text (visible or aria-label)
- Every input must have a label element with matching htmlFor
- Use semantic HTML elements before adding ARIA roles
- Modals: use dialog role, trap focus, Escape to close
- Lists: use ul/ol with li, not div with div
- Headings: maintain proper hierarchy (h1 → h2 → h3)
- Forms: group related fields with fieldset and legend
- Error messages: associate with inputs via aria-describedby
```

**Expected result:** Accessibility rules auto-attach for all component and page files.

## Complete code example

File: `src/components/shared/AccessibleDropdown.tsx`

```typescript
import { useState, useRef, useEffect, KeyboardEvent } from 'react';

interface DropdownProps {
  label: string;
  options: { value: string; label: string }[];
  value: string;
  onChange: (value: string) => void;
}

export function AccessibleDropdown({ label, options, value, onChange }: DropdownProps): JSX.Element {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  const selectedOption = options.find((o) => o.value === value);

  useEffect(() => {
    if (isOpen && listRef.current) {
      const active = listRef.current.children[activeIndex] as HTMLElement;
      active?.scrollIntoView({ block: 'nearest' });
    }
  }, [activeIndex, isOpen]);

  const handleKeyDown = (e: KeyboardEvent): void => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        if (!isOpen) setIsOpen(true);
        setActiveIndex((i) => Math.min(i + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex((i) => Math.max(i - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen && activeIndex >= 0) {
          onChange(options[activeIndex].value);
          setIsOpen(false);
          buttonRef.current?.focus();
        } else {
          setIsOpen(true);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        buttonRef.current?.focus();
        break;
    }
  };

  return (
    <div className="relative" onKeyDown={handleKeyDown}>
      <label id={`${label}-label`} className="block text-sm font-medium mb-1">
        {label}
      </label>
      <button
        ref={buttonRef}
        aria-haspopup="listbox"
        aria-expanded={isOpen}
        aria-labelledby={`${label}-label`}
        onClick={() => setIsOpen(!isOpen)}
        className="w-full border rounded px-3 py-2 text-left focus:ring-2"
      >
        {selectedOption?.label || 'Select an option'}
      </button>
      {isOpen && (
        <ul
          ref={listRef}
          role="listbox"
          aria-labelledby={`${label}-label`}
          aria-activedescendant={activeIndex >= 0 ? `option-${activeIndex}` : undefined}
          className="absolute w-full border rounded mt-1 bg-white shadow-lg z-10"
        >
          {options.map((option, index) => (
            <li
              key={option.value}
              id={`option-${index}`}
              role="option"
              aria-selected={option.value === value}
              className={`px-3 py-2 cursor-pointer ${index === activeIndex ? 'bg-blue-100' : ''} ${option.value === value ? 'font-bold' : ''}`}
              onClick={() => { onChange(option.value); setIsOpen(false); buttonRef.current?.focus(); }}
            >
              {option.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

## Common mistakes

- **Using div and span for all interactive elements** — Divs lack built-in keyboard handling, focus management, and screen reader semantics. Users who rely on assistive technology cannot interact with div-based buttons. Fix: Add a rule requiring semantic HTML: button for clickable actions, a for navigation, input for data entry. Only use divs for layout.
- **Adding empty alt text to decorative images only** — Cursor may add alt='' to all images. Informative images need descriptive alt text while only truly decorative images should have empty alt. Fix: Specify in your rules: 'Informative images need descriptive alt text. Only decorative images use alt="". Icons in buttons need aria-label on the button.'
- **Not testing keyboard navigation after generation** — ARIA attributes without proper keyboard event handlers create components that appear accessible but cannot be operated without a mouse. Fix: Always test Tab, Enter, Escape, and arrow key navigation after generating interactive components.

## Best practices

- Add WCAG 2.1 AA accessibility rules to .cursorrules with specific ARIA requirements
- Reference existing accessible components as templates when generating new ones
- Use semantic HTML elements before adding ARIA roles
- Test keyboard navigation on all generated interactive components
- Use @Lint Errors with eslint-plugin-jsx-a11y for post-generation auditing
- Include focus management rules for modals, dropdowns, and menus
- Create auto-attaching .cursor/rules/ for component directories with a11y requirements

## Frequently asked questions

### Does Cursor add ARIA attributes by default?

No. Cursor generates functional components but typically omits ARIA attributes, keyboard handlers, and focus management unless explicitly instructed. Add accessibility rules to .cursorrules for automatic enforcement.

### Can Cursor generate components that pass axe-core audits?

Yes, with proper rules. Add WCAG requirements to .cursorrules and audit generated components with @Lint Errors. For complex components, follow up with an explicit accessibility audit prompt.

### Should I use ARIA or semantic HTML?

Prefer semantic HTML first (button, nav, main, dialog). Add ARIA attributes only when HTML semantics are insufficient. Cursor follows this principle when your rules state: 'Use semantic HTML before ARIA roles.'

### How do I make Cursor generate keyboard-navigable components?

Add specific rules: 'All interactive elements must handle Enter, Escape, and arrow keys where appropriate.' Reference an existing keyboard-navigable component as a template with @file.

### Can Cursor help with color contrast checking?

Cursor cannot check actual rendered colors, but it can follow rules like 'Use text-gray-900 on white backgrounds for 4.5:1 contrast.' For real contrast checking, use browser DevTools or axe-core.

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-get-cursor-ai-to-integrate-accessibility-aria-attributes-in-react-component-suggestions
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-get-cursor-ai-to-integrate-accessibility-aria-attributes-in-react-component-suggestions
