DEV Community

Kedar Kulkarni
Kedar Kulkarni

Posted on

⚙️ ARIA for Developers – Roles, States, and Accessible Components in React

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

🧠 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

aria-labelledby (Reference another element's id):

<span id="label1">Username</span>
<input type="text" aria-labelledby="label1" />
Enter fullscreen mode Exit fullscreen mode

🔒 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)