In the first two parts of this series, we covered the fundamentals of accessibility and how to fix common issues like missing labels, bad contrast, and incorrect heading structure. Now, let’s dive deeper into ARIA (Accessible Rich Internet Applications) and how to use it with React to build dynamic and accessible web interfaces.
🔎 What Is ARIA?
WAI-ARIA is a W3C specification designed to enhance the accessibility of custom web components and dynamic content. When native HTML elements fall short—like custom dropdowns, modals, sliders, or toggles—ARIA roles and attributes bridge the gap by communicating additional semantics to assistive technologies.
⚠️ Before You Use ARIA…
🧠 “If you can use a native HTML element with the built-in behavior you need—use that instead of ARIA.”
Native elements like <button>
, <input>
, <nav>
, and <form>
already have accessibility features. Only use ARIA if there's no semantic HTML alternative.
🔧 ARIA Core Concepts
Feature | Description |
---|---|
Role | Defines what the element is (e.g., button , dialog ) |
State | Describes current conditions (e.g., aria-expanded="true" ) |
Property | Provides extra context (e.g., aria-labelledby="title" ) |
Let’s see how to apply these in a real-world project using React.
🧪 React + ARIA: Practical Examples
1️⃣ Toggle Button (Accessible Switch)
Here’s how to create a keyboard-operable toggle switch using ARIA.
import { useState } from "react"
function ToggleButton() {
const [isOn, setIsOn] = useState(false)
return (
<div
role="switch"
aria-checked={isOn}
tabIndex={0}
onClick={() => setIsOn(!isOn)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
setIsOn(!isOn)
}
}}
style={{
display: 'inline-block',
padding: '10px 20px',
background: isOn ? '#4caf50' : '#ccc',
borderRadius: '20px',
cursor: 'pointer',
userSelect: 'none'
}}
>
{isOn ? "ON" : "OFF"}
</div>
)
}
🧠 ARIA Highlights:
-
role="switch"
tells screen readers it’s a toggle. -
aria-checked
indicates current state. -
tabIndex={0}
makes the element focusable. - Arrow keys or spacebar trigger the toggle.
2️⃣ Custom Dropdown with Keyboard Support
import { useState, useRef, useEffect } from "react"
const options = ["Apple", "Banana", "Cherry"]
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
const [selected, setSelected] = useState(null)
const [highlightedIndex, setHighlightedIndex] = useState(0)
const dropdownRef = useRef(null)
useEffect(() => {
if (isOpen && dropdownRef.current) dropdownRef.current.focus()
}, [isOpen])
const handleKeyDown = (e) => {
if (!isOpen && (e.key === "Enter" || e.key === " ")) {
e.preventDefault()
setIsOpen(true)
return
}
if (isOpen) {
switch (e.key) {
case "ArrowDown":
e.preventDefault()
setHighlightedIndex((i) => (i + 1) % options.length)
break
case "ArrowUp":
e.preventDefault()
setHighlightedIndex((i) => (i - 1 + options.length) % options.length)
break
case "Enter":
e.preventDefault()
setSelected(options[highlightedIndex])
setIsOpen(false)
break
case "Escape":
setIsOpen(false)
break
}
}
}
return (
<div>
<div
role="button"
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-controls="dropdown-list"
tabIndex={0}
onClick={() => setIsOpen((o) => !o)}
onKeyDown={handleKeyDown}
style={{
padding: "10px",
background: "#eee",
border: "1px solid #ccc",
width: "200px",
cursor: "pointer",
}}
>
{selected || "Select an option"}
</div>
{isOpen && (
<ul
id="dropdown-list"
role="listbox"
ref={dropdownRef}
tabIndex={-1}
style={{
listStyle: "none",
margin: 0,
padding: 0,
border: "1px solid #ccc",
background: "white",
}}
>
{options.map((opt, i) => (
<li
key={opt}
role="option"
aria-selected={highlightedIndex === i}
onClick={() => {
setSelected(opt)
setIsOpen(false)
}}
style={{
padding: "10px",
background: highlightedIndex === i ? "#007bff" : "white",
color: highlightedIndex === i ? "white" : "black",
}}
>
{opt}
</li>
))}
</ul>
)}
</div>
)
}
ARIA Usage:
-
role="button"
: Declares the trigger element -
aria-expanded
: Shows whether dropdown is open -
aria-haspopup="listbox"
: Identifies a related popup -
role="listbox"
&role="option"
: Define the dropdown structure
3️⃣ Accessible Labeling: aria-label
& aria-labelledby
Sometimes visible labels aren’t possible. ARIA attributes help provide accessible names.
aria-label
(Custom label directly on the element):
<input type="text" aria-label="First name" />
aria-labelledby
(Reference another element's id
):
<span id="label1">Username</span>
<input type="text" aria-labelledby="label1" />
🔒 ARIA Do’s and Don’ts
✅ Do This | ❌ Don’t Do This |
---|---|
Use semantic HTML if available | Replace native elements with divs + ARIA |
Make ARIA widgets keyboard-accessible | Ignore focus/keyboard behavior |
Test with screen readers & WAVE | Assume ARIA works out-of-the-box |
Use aria-hidden="true" sparingly |
Apply it to interactive or focusable elements |
🧠 Key ARIA Roles You Should Know
Role | Use Case |
---|---|
button |
Interactive element |
dialog |
Modal popup |
tab , tabpanel
|
Tab interfaces |
listbox / option
|
Dropdowns or selections |
alert |
Announce important changes |
🧩 Full list: ARIA Roles Overview – MDN
📋 Summary: Building Accessible Components with ARIA
Topic | Key Point |
---|---|
Use ARIA selectively | Prefer native elements when possible |
Labeling matters | Use aria-label or aria-labelledby to name elements |
Manage state | Use aria-expanded , aria-checked , etc. to reflect status |
Support keyboards | Always provide tabIndex and onKeyDown handlers |
Validate early | Test with WAVE, NVDA, VoiceOver |
Top comments (0)