Suggest an editImprove this articleRefine the answer for “Web accessibility and ARIA attributes”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Web accessibility** (a11y) means building websites that work for people with disabilities - screen reader users, keyboard-only navigators, and others. Semantic HTML handles most cases automatically. ARIA fills the gaps for dynamic content and custom widgets that have no native HTML equivalent. ```html <div role="alert" aria-live="polite">Form saved!</div> ``` **Key:** reach for ARIA only when semantic HTML has no matching element.Shown above the full answer for quick recall.Answer (EN)Image**Web accessibility** (a11y) is the practice of building websites that work for people using screen readers, keyboard-only navigation, and other assistive technologies. ARIA attributes are the tool you reach for when semantic HTML alone doesn't carry enough information. ## Theory ### TL;DR - ARIA is like braille labels on a museum exhibit: the exhibit (HTML) already describes itself, but braille fills gaps for visitors who need it - Semantic HTML elements (`<button>`, `<nav>`, `<main>`) tell screen readers what they are automatically; ARIA does the same for generic `<div>`s and custom widgets - Decision rule: reach for ARIA only when HTML has no matching native element for what you're building - Dynamic alerts, modals, and sliders almost always need ARIA; static content usually doesn't ### Quick example ```html <!-- Native HTML: screen reader says "Submit, button" with zero extra code --> <button>Submit</button> <!-- ARIA fills the gap for dynamic content injected by JS --> <div role="alert" aria-live="polite">Form submitted!</div> <!-- Screen reader announces this immediately, even when JS adds it to the DOM --> <!-- Custom modal: HTML has no native equivalent --> <div role="dialog" aria-modal="true" aria-labelledby="modal-title"> <h2 id="modal-title">Confirm Action</h2> <button>Close</button> </div> ``` The first example needs nothing. The second two show where ARIA earns its place. ### Key difference Semantic HTML elements map directly to roles in the browser's Accessibility Tree. `<button>` becomes a node with `role="button"`, a computed name from its text content, and built-in keyboard behavior - no extra code needed. ARIA attributes like `role="button"` or `aria-expanded` write to the same tree manually, but they don't add behavior. A `<div role="button">` without `tabindex="0"` isn't focusable. Without a `keydown` handler, Enter and Space do nothing. Native HTML handles all of that automatically. ### When to use ARIA - Static pages with forms, links, and navigation: semantic HTML only - Error messages or status updates added by JS: `role="alert"` or `aria-live="polite"` on the container - Modals: `role="dialog"` + `aria-modal="true"` + `aria-labelledby` pointing to the title element - Custom widgets with no HTML equivalent (sliders, tree views, tab panels): ARIA roles combined with keyboard event handlers - Decorative icons inside labeled buttons: `aria-hidden="true"` to prevent double-reading - Skip ARIA when a native semantic tag already covers the use case ### How the accessibility tree works Browsers parse HTML and build the Accessibility Tree alongside the DOM. Screen readers query it through OS-level APIs: Microsoft's UI Automation on Windows, Apple's Accessibility API on macOS and iOS. `<button>` gets a node with role, name, and state already set. ARIA attributes override or extend those properties at render time - Blink and WebKit inject `aria-expanded` or `aria-valuenow` directly into the tree. Live regions work differently. When JS mutates a live region's content, the browser sends a tree diff notification to the screen reader right away, without waiting for the user to navigate there. That's why adding text to a plain `<div>` is silent, but the same change inside `<div aria-live="polite">` gets announced. ### Common mistakes **Adding `role="button"` to a `<button>`:** ```html <button role="button">Click</button> ``` No real harm, but screen readers ignore the redundant role. Remove it and use native semantics. **Using `aria-label` alongside a visible `<label>`:** ```html <label>Username: <input aria-label="Username input" /></label> <!-- Screen reader reads: "Username input, input" — the two labels collide --> ``` Let the `<label>` element do the work. Remove `aria-label` when a proper label association already exists. **Forgetting `aria-live` on dynamic updates:** ```jsx setError('Invalid email'); // <div id="error">{error}</div> — silent to screen readers ``` Fix: add `role="alert"` or `aria-live="assertive"` to the container before any error content lands in it. **`aria-expanded` without toggling the value in JS:** The attribute announces expansion state to screen readers. If the JS never flips it from `false` to `true`, the reader tells the user the menu opened when it didn't. Sync the attribute on every interaction. ### Real-world usage - React: Material UI tabs use `role="tablist"` with `aria-selected` on each tab item - Next.js: `aria-current="page"` on the active `<Link>` for navigation landmarks - Vue: Vuetify data tables add `aria-sort` to sortable column headers - Angular CDK: overlays apply `aria-label` via `ng-attr-aria-label` - Testing: Chrome DevTools Accessibility pane shows the computed tree; axe-core catches missing labels and incorrect roles automatically ### Follow-up questions **Q:** What's the difference between `role="alert"` and `aria-live="polite"`? **A:** `role="alert"` sets `aria-live="assertive"` automatically and interrupts whatever the screen reader is currently reading. Use it for error messages. `aria-live="polite"` queues the announcement until the reader finishes its current sentence - good for non-urgent status messages. **Q:** How does `aria-hidden="true"` differ from `display: none`? **A:** `aria-hidden` removes an element from the Accessibility Tree while keeping it visible in the DOM. Use it for decorative icons or animations you don't want read aloud. `display: none` removes it from both the visual layout and the Accessibility Tree completely. **Q:** Why is `tabindex="0"` on a `<div>` a problem? **A:** It makes the element focusable without giving it any interactive role, keyboard behavior, or ARIA state. Tab order hits a dead end for keyboard users. Only add `tabindex="0"` when building a full custom widget with all the keyboard handling that goes with it. **Q:** Explain WCAG 4.1.2 Name, Role, Value. **A:** Assistive tech needs three things from every interactive element: its name (from text content, `aria-label`, or `aria-labelledby`), its role (from the HTML tag or `role` attribute), and its current state or value (`aria-valuenow`, `aria-checked`, `aria-expanded`). ARIA makes this possible for elements HTML alone can't describe. **Q:** (Senior) In a React portal modal, how does focus trapping work with ARIA, and what can go wrong with SSR hydration? **A:** Set `aria-modal="true"` so screen readers treat the dialog as the only active region, then add a focus trap that intercepts Tab and Shift+Tab. On hydration mismatch, the portal may not exist in the initial server-rendered HTML, so the modal mounts without focus. Fix: call `containerRef.current.focus()` inside `useEffect` after mount, and apply the `inert` attribute to background content. VoiceOver on Safari has a known issue where focus escapes portals - always test that combination separately. ## Examples ### Semantic HTML vs ARIA roles ```html <!-- Static nav: semantic HTML, no ARIA needed --> <nav> <a href="/" aria-current="page">Home</a> <a href="/about">About</a> </nav> <!-- Dynamic error: ARIA live region required --> <form> <label for="email">Email</label> <input type="email" id="email" aria-describedby="email-error" /> <!-- role="alert" makes this announce the moment error text appears --> <div id="email-error" role="alert" aria-live="assertive"></div> </form> <!-- Decorative icon inside a labeled button --> <button aria-label="Close dialog"> <svg aria-hidden="true" focusable="false">...</svg> </button> ``` The nav example uses `aria-current="page"` on the active link. That's ARIA, but it's the right call because HTML has no native "current page" concept. Everything else is plain semantic HTML doing its job. ### Accessible modal in React ```jsx import { useState, useEffect, useRef } from 'react'; function Modal() { const [isOpen, setIsOpen] = useState(false); const modalRef = useRef(null); useEffect(() => { // Move focus into the modal when it opens if (isOpen) modalRef.current?.focus(); }, [isOpen]); return ( <> <button onClick={() => setIsOpen(true)}>Delete account</button> {isOpen && ( <div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} style={{ position: 'fixed', background: 'white', padding: 24 }} > <h2 id="modal-title">Are you sure?</h2> <p>This action cannot be undone.</p> <button onClick={() => setIsOpen(false)}>Cancel</button> <button>Confirm</button> </div> )} </> ); } // NVDA announces "dialog Are you sure?" when the modal opens. // aria-modal tells the reader to ignore content behind the dialog. ``` One thing I always add in production: an Escape key handler to close the modal and a focus trap that keeps Tab cycling inside the dialog. The ARIA alone isn't enough for complete keyboard accessibility. ### Custom ARIA slider with keyboard support ```html <div role="slider" tabindex="0" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-label="Volume" style="width: 200px; height: 20px; background: #ddd;" ></div> <script> const slider = document.querySelector('[role="slider"]'); slider.addEventListener('keydown', (e) => { let val = +slider.getAttribute('aria-valuenow'); if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); // stops the page from scrolling slider.setAttribute('aria-valuenow', Math.min(val + 10, 100)); } if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); slider.setAttribute('aria-valuenow', Math.max(val - 10, 0)); } }); </script> ``` The screen reader announces the new value each time `aria-valuenow` changes. Without `e.preventDefault()`, arrow keys scroll the page while updating the slider - a bug that's easy to miss in visual testing but immediately obvious when using NVDA.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.