Skip to main content
    Courses/HTML & CSS/Accessibility Deep Dive & ARIA

    Lesson 17 • Advanced Track

    Accessibility Deep Dive & ARIA Roles

    By the end of this lesson you'll know when ARIA helps, when it hurts, and how to bolt correct roles, states, and live regions onto custom widgets without breaking screen readers.

    What You'll Learn

    You'll be able to explain ARIA roles, states, and properties
    You'll apply the first rule of ARIA — prefer semantic HTML
    You'll name elements with aria-label, aria-labelledby & aria-describedby
    You'll use aria-expanded, aria-hidden & aria-current correctly
    You'll announce dynamic updates with aria-live regions
    You'll add roles to custom widgets like tabs and menus
    Before this lesson: you should be comfortable with semantic HTML elements like <button>, <nav> and <label>. If those feel shaky, revisit HTML5 Semantic Architecture first — ARIA builds directly on top of it.

    💡 Think of It Like This

    ARIA is like subtitles on a film. The film (your page) already has visual cues, and subtitles (ARIA) make that same information available to people who can't see the screen. But here's the catch: ARIA only writes the subtitle — it never changes the film. Slapping role="button" on a <div> tells the screen reader "this is a button" but adds zero clicking, focusing, or keyboard behaviour. A real <button> brings the subtitle and the behaviour for free.

    The first rule of ARIA: don't use ARIA. If a native HTML element gives you the role and behaviour you need, use it instead.

    Instead of ARIA…Use native HTML
    <div role="button"><button>
    <div role="navigation"><nav>
    <span role="heading"><h1>–<h6>
    <div role="link"><a href>
    <div role="checkbox"><input type="checkbox">

    1. Roles, States & Properties — The Three Jobs of ARIA

    ARIA stands for Accessible Rich Internet Applications. It does exactly three things, and it's worth keeping them straight:

    • Role — what the thing is: role="tab", role="alert".
    • State — a condition that changes as the user interacts: aria-expanded, aria-checked, aria-hidden.
    • Property — a fixed characteristic that rarely changes: aria-label, aria-haspopup.

    The example below puts all three on one custom toggle so you can see how they read out. Notice the comments stating exactly what a screen reader announces.

    Roles, States & Properties Together

    One toggle showing a role, a changing state, and a fixed property

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Role, State, Property</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        .switch {
          background:#334155; color:#fff; border:none; padding:12px 18px;
          border-radius:6px; cursor:pointer; font-size:15px;
        }
        .switch[aria-pressed="true"] { background:#22c55e; color:#0f172a; }
        .note { color:#94a3b8; font-size:13px; margin-top:10px; }
        code
    ...

    2. Naming Elements: aria-label, aria-labelledby & aria-describedby

    Every interactive element needs an accessible name — the text a screen reader speaks. Three attributes give you one:

    • aria-label — type the name directly. Use it when there's no visible text (an icon button).
    • aria-labelledby — point at the id of existing visible text, so the name stays in sync.
    • aria-describedby — point at extra help text spoken after the name (a hint, not the name itself).

    ARIA Labelling Techniques

    Give every control a clear accessible name

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>ARIA Labels</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        .example { background:#1e293b; padding:20px; border-radius:8px; margin:15px 0; }
        h3 { color:#22c55e; margin:20px 0 8px; font-size:1rem; }
        button { background:#3b82f6; color:#fff; border:none; padding:10px; border-radius:6px;
                 cursor:pointer; width:40px; height:40px; font-siz
    ...

    3. Common States: aria-expanded, aria-hidden & aria-current

    States communicate what's happening right now. The three you'll reach for most:

    • aria-expanded — is this menu/accordion open? Flip it with JavaScript on each toggle.
    • aria-hidden="true" — hide decorative content from screen readers. Never put it on anything focusable.
    • aria-current="page" — mark the current item in a nav, pagination, or breadcrumb.

    States in Action

    A disclosure button, a decorative icon, and a current nav item

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>ARIA States</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        .disclosure { background:#334155; color:#fff; border:none; padding:10px 16px;
                      border-radius:6px; cursor:pointer; font-size:15px; }
        .panel { background:#1e293b; padding:14px; border-radius:6px; margin-top:8px; }
        nav a { color:#94a3b8; text-decoration:none; margin-right:
    ...

    4. Live Regions — Announcing Dynamic Content

    When content changes without a page reload — a "saved" toast, a form error, a cart count — sighted users see it, but a screen reader stays silent unless you mark the container as a live region. Use aria-live="polite" for routine updates and aria-live="assertive" (or role="alert") for urgent errors that should interrupt.

    ARIA Live Regions

    Polite updates, assertive alerts, and role=status

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Live Regions</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        button { background:#3b82f6; color:#fff; border:none; padding:10px 18px; border-radius:6px;
                 cursor:pointer; font-size:14px; margin:5px 0; }
        .status { background:#14532d; border:1px solid #22c55e; padding:12px; border-radius:6px; margin-top:10px; }
        .alert  { background:#7f1
    ...

    5. Roles for Custom Widgets: Tabs & Menus

    Some widgets — tabs, menus, sliders — have no native HTML element. Here ARIA earns its keep: you build the structure from <div>s, then describe it with a coordinated set of roles and states (a "design pattern"). A tab set needs role="tablist", role="tab" with aria-selected, and role="tabpanel".

    Accessible Tabs

    A tab widget wired up with the correct roles and states

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Accessible Tabs</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        [role="tablist"] { display:flex; gap:4px; border-bottom:1px solid #334155; }
        [role="tab"] { background:none; color:#94a3b8; border:none; padding:10px 16px;
                       cursor:pointer; font-size:15px; }
        [role="tab"][aria-selected="true"] { color:#22c55e; border-bottom:2px soli
    ...
    💡 A full menu widget (role="menu" / role="menuitem") also needs arrow-key navigation. That's a lot of JavaScript — which is exactly why a plain <nav> of <a> links is better whenever it fits.

    🎯 Your Turn #1 — Name the Icon Buttons

    These icon-only buttons are silent to a screen reader. Add an accessible name to each by filling in the blanks. One concept, one minute.

    🎯 Your Turn: Add Accessible Names

    Fill in the aria-label blanks marked with ___

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Your Turn — Labels</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        button { background:#3b82f6; color:#fff; border:none; width:44px; height:44px;
                 border-radius:6px; cursor:pointer; font-size:20px; margin:6px; }
      </style>
    </head>
    <body>
      <h1>Toolbar</h1>
    
      <!-- 🎯 YOUR TURN — replace each ___ with a clear aria-label -->
    
      <!-- 1) This tr
    ...

    🎯 Your Turn #2 — Wire Up aria-expanded

    This "Read more" disclosure shows and hides a panel, but the screen reader never learns whether it's open. Add the missing state and keep it in sync.

    🎯 Your Turn: Announce Open/Closed

    Add aria-expanded and update it in the blanks marked with ___

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Your Turn — Expanded</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        button { background:#334155; color:#fff; border:none; padding:10px 16px;
                 border-radius:6px; cursor:pointer; font-size:15px; }
        .panel { background:#1e293b; padding:14px; border-radius:6px; margin-top:8px; }
      </style>
    </head>
    <body>
      <h1>FAQ</h1>
    
      <!-- 🎯 YOUR TURN —
    ...

    🧩 Mini-Challenge — Accessible "Saved" Toast

    Support is faded now — only an outline is given. Build a button that, when clicked, makes a live region announce that the form was saved. Use what you learned in Section 4.

    🧩 Mini-Challenge: Live Toast

    Only a comment outline is provided — you write the code

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Mini-Challenge — Toast</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family:system-ui, sans-serif; padding:30px; }
        button { background:#22c55e; color:#0f172a; border:none; padding:10px 18px;
                 border-radius:6px; cursor:pointer; font-weight:600; }
        .toast { background:#14532d; border:1px solid #22c55e; padding:12px;
                 border-radius:6px; margin-top:12px; min-height:20px; }
    
    ...

    ⚠️ Common Errors (and the Fix)

    • Redundant ARIA on semantic elements. <button role="button"> or <nav role="navigation"> repeat a role the element already has — and some redundant roles actively suppress native behaviour. Fix: drop the role; the native element already carries it.
    • aria-hidden on a focusable element. Putting aria-hidden="true" on (or around) a link or button creates a "ghost" stop — keyboard users Tab to it but screen readers say nothing. Fix: never hide focusable content; if it must be hidden, remove it from the tab order too (e.g. with the hidden attribute or tabindex="-1" plus removal).
    • Wrong or invented roles. role="text" on a heading, or role="button" on something that scrolls, lies to the user about what the element does. Fix: use a real role from the ARIA spec that matches the actual behaviour — or a native element instead.
    • aria-label ignored on a non-interactive element. Browsers often drop aria-label on a plain <span> or <div> with no role. Fix: put labels on elements that take a name (buttons, links, inputs, or things with a widget/landmark role).

    📋 ARIA Quick Reference

    Attribute / roleKindWhat it does
    aria-labelPropertySets the accessible name directly (no visible text)
    aria-labelledbyPropertyNames an element from another element's id
    aria-describedbyPropertyAdds help text spoken after the name
    aria-expandedStateWhether a disclosure / menu is open
    aria-hiddenStateHides decorative content from screen readers
    aria-currentStateMarks the current item (page, step, date)
    aria-liveProperty"polite" or "assertive" — announce changes
    role="alert"RoleAssertive live region for urgent errors
    role="tablist"/"tab"/"tabpanel"RoleStructure for a custom tab widget

    💡 Pro tip: before adding any of these, ask "is there a native element that already does this?" — the answer is usually yes.

    ❓ Frequently Asked Questions

    What is the difference between a role, a state, and a property in ARIA?

    A role tells assistive tech what a thing IS (role="tab", role="alert"). A state describes a condition that changes as the user interacts (aria-expanded, aria-checked, aria-hidden). A property describes a fixed characteristic that rarely changes (aria-label, aria-labelledby, aria-haspopup). Roles and properties are usually set once; states are updated with JavaScript.

    Why is 'the first rule of ARIA: don't use ARIA' the most important rule?

    Native HTML elements like <button>, <nav>, <a href> and <input> already carry the correct role, keyboard behaviour, and focus handling for free. ARIA changes only how a screen reader describes an element — it adds zero behaviour. So a <div role="button"> still needs you to wire up Tab focus, Enter/Space keys, and a focus ring by hand, and most people forget at least one. Reaching for the native element is less code and fewer bugs.

    When should I use aria-label versus aria-labelledby?

    Use aria-label when there is NO visible text to name the element — for example an icon-only button (aria-label="Close"). Use aria-labelledby when the name already exists as visible text somewhere on the page; you point it at that element's id so the name stays in sync. If both are present, aria-labelledby wins.

    What does aria-hidden="true" do, and what is the trap?

    aria-hidden="true" removes an element and all its children from the accessibility tree, so screen readers skip it — useful for decorative icons. The trap: it does NOT remove the element from keyboard focus. If you hide a container that holds a focusable button, a keyboard user can still Tab to a button that screen readers cannot announce — a confusing dead spot. Never put aria-hidden on, or around, anything focusable.

    What is an aria-live region and when do I need one?

    A live region is an element with aria-live (or role="status"/role="alert") that tells the screen reader to announce changes to its content automatically. You need one whenever content updates without a page reload — form errors, search-result counts, 'saved' toasts. Use aria-live="polite" for non-urgent updates and aria-live="assertive" (or role="alert") for errors that must interrupt.

    Do I still need to handle the keyboard if I add a role to a div?

    Yes. ARIA roles only change the spoken label — they add no behaviour. A <div role="button"> needs tabindex="0" to be focusable and JavaScript to respond to Enter and Space. This is exactly why native <button> is preferred: it does all of that automatically.

    🎉 Lesson Complete

    You can now add ARIA the way pros do — sparingly and correctly:

    • ✅ Native HTML first — ARIA changes the label, never the behaviour
    • ✅ Name controls with aria-label / aria-labelledby, add hints with aria-describedby
    • ✅ Track conditions with aria-expanded, aria-hidden and aria-current
    • ✅ Announce dynamic updates through aria-live regions
    • ✅ Reach for widget roles (tabs, menus) only when no native element fits

    Up next: Complex Forms & Validation — where good labelling and live regions make error handling genuinely usable.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.