AI Either Ignores ARIA or Sprinkles It Like Magic Dust
AI generates ARIA usage with: no ARIA at all (custom widgets that screen readers cannot understand), ARIA on everything (role="button" on a <button> — redundant and confusing), incorrect ARIA (aria-expanded on an element that does not expand anything), conflicting ARIA (role="button" on an <a href> — the link semantics and button role contradict each other), and ARIA without behavior (aria-expanded="true" announced but no keyboard interaction to actually expand). ARIA without correct implementation is worse than no ARIA — it tells screen readers something that is not true.
Correct ARIA usage follows the five rules of ARIA: (1) If you can use a native HTML element, use it instead of ARIA. (2) Do not change native semantics unless absolutely necessary. (3) All interactive ARIA controls must be keyboard-operable. (4) Do not use role="presentation" or aria-hidden="true" on visible focusable elements. (5) All interactive elements must have accessible names. AI violates all five rules by default.
These rules cover: the five rules of ARIA, correct role assignments for custom widgets, state and property attributes, live regions for dynamic content, accessible names, and the most common ARIA anti-patterns.
Rule 1: The Five Rules of ARIA
The rule: 'Rule 1: Use native HTML instead of ARIA when possible. <button> instead of <div role="button">. <nav> instead of <div role="navigation">. <input type="checkbox"> instead of <div role="checkbox">. Native elements provide: keyboard behavior, focus management, form submission, and screen reader semantics — all for free. ARIA roles require you to reimplement all of these manually.'
Rules 2-5: '(2) Do not override native semantics: never <h2 role="tab"> (the heading semantics are lost). Use <div role="tab"> instead. (3) Every interactive ARIA widget must be keyboard-operable: if role="button" then Enter and Space must activate it. If role="tablist" then arrow keys must navigate between tabs. (4) Never aria-hidden="true" or role="presentation" on visible, focusable elements — they become invisible to screen readers but still receive focus (a trap). (5) Every interactive element must have an accessible name: aria-label, aria-labelledby, or visible text content.'
AI generates: <div role="button" onClick={handler}>Submit</div> — violates Rule 1 (use <button>), likely violates Rule 3 (no keyboard handling for Enter/Space), and adds unnecessary ARIA when a native element would work. Replace with <button onClick={handler}>Submit</button> — zero ARIA needed, keyboard behavior included, screen reader announcement correct. The best ARIA is the ARIA you do not need because the native element handles it.
- Rule 1: native HTML over ARIA — <button> not <div role='button'>
- Rule 2: do not override native semantics — never <h2 role='tab'>
- Rule 3: interactive ARIA must be keyboard-operable — role='button' needs Enter/Space
- Rule 4: never aria-hidden on visible focusable elements — invisible trap
- Rule 5: every interactive element needs an accessible name — label or aria-label
<div role='button' onClick>: violates Rule 1 (use native), needs manual keyboard handling, needs manual focus management. <button onClick>: zero ARIA needed, keyboard included, screen reader correct. The best ARIA is the ARIA you do not need because the native element handles it.
Rule 2: Correct Role Assignments for Custom Widgets
The rule: 'When native HTML elements cannot represent the widget, assign the correct ARIA role. Common widgets and their roles: custom dropdown → role="listbox" with role="option" children. Tab interface → role="tablist" with role="tab" children and role="tabpanel" for content. Modal dialog → role="dialog" with aria-modal="true". Disclosure (expand/collapse) → button with aria-expanded controlling a region. Toolbar → role="toolbar" with grouped interactive children. Each role has required properties, required keyboard interactions, and expected child roles defined in WAI-ARIA Authoring Practices.'
For role hierarchy: 'Some roles require specific parent-child relationships. role="tab" must be inside role="tablist". role="option" must be inside role="listbox". role="menuitem" must be inside role="menu". Violating the hierarchy: the screen reader does not understand the relationship. A role="tab" outside of a role="tablist" is announced as a tab but has no context (which tablist? how many tabs? which is selected?). The hierarchy provides context that individual roles cannot.'
AI generates: role="menu" for a navigation bar (menus have specific keyboard expectations — arrow keys, Escape, typeahead — that a nav bar does not implement). Or: no roles on a custom dropdown (screen reader announces it as a generic div, not a selectable list). Correct role assignment: the screen reader announces "listbox, 5 options, option 2 of 5 selected." The user understands the widget type, the item count, and their current position — without seeing the screen.
Rule 3: State and Property Attributes
The rule: 'ARIA states communicate the current condition of an element. aria-expanded (true/false): the controlled section is expanded or collapsed. aria-selected (true/false): the tab, option, or item is selected. aria-checked (true/false/mixed): the checkbox is checked, unchecked, or indeterminate. aria-disabled (true): the element is not interactive (but remains visible and focusable — unlike the HTML disabled attribute which removes from tab order). aria-pressed (true/false): the toggle button is pressed (active) or not. Update states dynamically: when the user expands a section, set aria-expanded="true". Screen readers announce the state change.'
For aria-controls and aria-describedby: 'aria-controls="panel-1": the button controls the element with id="panel-1" (screen readers may offer navigation to the controlled element). aria-describedby="error-msg": the input is described by the element with id="error-msg" (screen readers read both the label and the description). aria-labelledby="title-1 subtitle-1": the element is labeled by multiple elements (IDs space-separated — both texts are read as the label). These relationship attributes connect elements that are visually associated but not programmatically linked.'
AI generates: aria-expanded on a button but never updates it (always "false" even when the section is open — screen reader announces "collapsed" when the content is visible). Or: no aria-expanded at all (screen reader does not know the section can be toggled). State attributes must reflect the actual state in real-time. Every user interaction that changes the visual state must update the corresponding ARIA state. The announced state and the visual state must always match.
- aria-expanded: toggle on expand/collapse — must reflect actual visual state
- aria-selected: on tabs, listbox options — announce which item is active
- aria-checked: checkboxes and toggles — true, false, or mixed for indeterminate
- aria-controls: link button to the panel it controls — screen reader navigation
- aria-describedby: link input to error message or help text — read as description
aria-expanded='false' on a button while the section is visually open: the screen reader announces 'collapsed' for expanded content. Every user interaction that changes visual state must update the ARIA state. Announced state and visual state must always match.
Rule 4: Live Regions for Dynamic Content
The rule: 'When content updates dynamically without a page reload, screen readers do not know it changed. aria-live regions announce changes: aria-live="polite" (announce when the user is idle — use for most updates), aria-live="assertive" (announce immediately, interrupting the current announcement — use for critical alerts only), role="alert" (shorthand for aria-live="assertive" + aria-atomic="true" — the entire region is re-announced). Add aria-live to the container before the content changes — the element must exist with aria-live before the dynamic content is inserted.'
For common live region use cases: 'Toast notifications: <div role="alert">Your changes have been saved.</div> (announced immediately). Form validation: <div aria-live="polite" id="email-error">Email format is invalid.</div> (announced when idle — does not interrupt typing). Loading state: <div aria-live="polite">Loading results...</div> then <div aria-live="polite">15 results found.</div> (transition announced). Chat messages: <div aria-live="polite">{lastMessage}</div> (new messages announced as they arrive).'
AI generates: a toast notification that pops up visually but is invisible to screen readers (no aria-live, no role="alert" — the dynamic DOM change is silent). The user saves a form: sighted users see "Saved!" toast. Screen reader users hear nothing — they do not know the save succeeded. With role="alert": the screen reader announces "Your changes have been saved" immediately. One attribute makes dynamic feedback accessible.
Toast notification pops up visually but screen readers hear nothing. Add role='alert': the screen reader announces 'Your changes have been saved' immediately. One attribute makes dynamic feedback accessible to every user.
Rule 5: Common ARIA Anti-Patterns to Avoid
The rule: 'Anti-pattern 1: role="button" on <button> (redundant — <button> already has the button role). Anti-pattern 2: aria-label that duplicates visible text (aria-label="Submit" on a button that already says "Submit" — screen reader announces "Submit" twice or the aria-label overrides the visible text). Anti-pattern 3: aria-hidden="true" on content that is visible (hides content from screen readers but visible to sighted users — creates an inconsistent experience). Anti-pattern 4: role="presentation" on interactive elements (removes semantics from a button or link — makes it invisible to assistive technology while remaining visually interactive).'
For the aria-label vs visible text rule: 'aria-label overrides the element visible text for screen readers. <button aria-label="Close">X</button> — screen reader announces "Close button" (good — "X" alone is meaningless). <button aria-label="Submit form">Submit</button> — screen reader announces "Submit form button" which differs from the visible "Submit" text. Voice control users say "Click Submit" but the accessible name is "Submit form" — the command may not match. Use aria-label only when the visible text is insufficient or absent. When visible text is clear, do not add aria-label.'
AI generates: ARIA on everything "just to be safe." role="button" on buttons, role="link" on links, aria-label duplicating visible text, role="img" on <img> elements. Each redundant attribute adds noise. Screen readers process ARIA verbosely — redundant attributes cause double-announcements, conflicting information, and a cluttered experience. The safest ARIA strategy: native HTML first, ARIA only for gaps, test with a screen reader before shipping.
Complete ARIA Attributes Rules Template
Consolidated rules for ARIA attributes.
- Five rules: native HTML first, do not override semantics, keyboard required, no hidden focusable, name required
- Correct roles: listbox/option, tablist/tab/tabpanel, dialog, toolbar — per WAI-ARIA Practices
- States reflect reality: aria-expanded, aria-selected, aria-checked — update on every interaction
- Live regions: aria-live='polite' for updates, role='alert' for critical — container exists before content
- No redundant ARIA: do not add role='button' to <button> or aria-label duplicating visible text
- aria-label only when visible text is insufficient: 'X' needs aria-label='Close', 'Submit' does not
- Never aria-hidden on visible focusable elements — invisible to screen reader but receives focus
- Test with screen reader before shipping: NVDA (free, Windows), VoiceOver (built-in, Mac/iOS)