Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 15 min read 👁 2 views 📅 Oct 21, 2026

ARIA: Making Custom Components Accessible

Learn when and how to use ARIA roles, names, states and live regions so that custom FAQs, tabs and status messages work with the keyboard and screen readers.

The College Website That Ravi Could Not Use

Ravi is a first-year student who is blind. He uses a screen reader, a program that reads the screen aloud and lets him move around with the keyboard. When admissions opened, he visited his college website to check the hostel rules and the list of courses.

The page had an FAQ section where you click a question to see the answer, and a row of course tabs: Science, Commerce, Arts. Sighted students clicked them happily. For Ravi, the screen reader said only “When does admission open, text”. Pressing Enter did nothing. The tabs were plain <div> elements with click handlers, so the keyboard could not even reach them.

The developer had built custom components: interactive widgets made from plain boxes and JavaScript. In this lesson you will learn how to make such components work for everyone, using ARIA (Accessible Rich Internet Applications). ARIA is a set of HTML attributes that tell assistive technology what an element is, what state it is in and how it relates to other elements. You already know the basics from the Accessibility Basics lesson. This lesson is the next step.

The First Rule of ARIA: Use Native HTML First

Here is the most important idea in this lesson: if a native HTML element already does the job, use it instead of adding ARIA. A real <button> gets keyboard focus, responds to Enter and Space, and is announced as “button”, all for free. A <div> gets none of this. ARIA only changes what the screen reader says. It never adds keyboard behaviour.

Run this example. Click both with the mouse. Then click in the result area and press Tab: the focus jumps straight past the fake button to the real one.

<style>
  .fake-btn { background: #1e6fd9; color: white; padding: 8px 14px; display: inline-block; cursor: pointer; }
</style>

<!-- A div pretending to be a button: mouse users can click it, keyboard users cannot reach it -->
<div class="fake-btn" onclick="this.textContent = 'Applied!'">Apply for Admission</div>

<!-- A real button: works with mouse, Tab, Enter and Space, and screen readers call it a button -->
<button type="button" onclick="this.textContent = 'Applied!'">Apply for Admission</button>

For an FAQ, HTML already has a ready-made disclosure widget: <details> and <summary>. It opens and closes with mouse and keyboard, and screen readers announce whether it is expanded or collapsed. No JavaScript and no ARIA needed.

<h2>Frequently Asked Questions</h2>

<details>
  <summary>When does the B.Sc. admission open?</summary>
  <p>Online applications open on 1 June and close on 30 June.</p>
</details>

<details>
  <summary>Is there a hostel for first-year students?</summary>
  <p>Yes. Separate hostels for boys and girls are available inside the campus.</p>
</details>
Why this rule matters: bad ARIA is worse than no ARIA. A wrong role makes a screen reader promise something the element cannot do, and the user gets stuck.

Roles: Telling the Screen Reader What Something Is

Every element has a role, which is its job. A <button> has the role “button”, a <nav> has the role “navigation”, and a <div> has no meaningful role at all. The role attribute lets you set a role when no native element fits. Common examples:

RoleUse it forNative element to prefer
buttonSomething that does an action<button>
tablist, tab, tabpanelA tab widgetNone exists, so ARIA is right here
statusA polite message that updates<output> in some cases
alertAn urgent message, read at onceNone
imgA group of things that form one picture<img> with alt

Tabs are a good example of when ARIA is the correct tool. HTML has no tab element, so you build tabs from buttons and add role="tab". You will build them in the real-life example below.

Names and Descriptions: aria-label, aria-labelledby, aria-describedby

Every interactive element needs an accessible name: the words a screen reader speaks for it. Usually the name comes from the visible text, like the text inside a button or the <label> of an input. When there is no visible text, ARIA helps:

  • aria-label="..." gives a name directly as a string. Use it for icon-only buttons.
  • aria-labelledby="id" takes the name from another element on the page. Prefer this when the text already exists, because it stays in sync and gets translated with the page.
  • aria-describedby="id" adds extra help, such as a hint or an error message. The screen reader reads it after the name.
<!-- aria-label: the button shows only an icon, so we give it a name -->
<button type="button" aria-label="Search courses">&#128269;</button>

<!-- aria-labelledby: the name comes from text that is already on the page -->
<h2 id="fees-title">Fee Details</h2>
<table aria-labelledby="fees-title">
  <tr><th>Course</th><th>Fee per year</th></tr>
  <tr><td>B.Com</td><td>&#8377;18,000</td></tr>
</table>

<!-- aria-describedby: extra help text that is read after the name -->
<label for="roll">Roll number</label>
<input id="roll" type="text" aria-describedby="roll-help">
<p id="roll-help">Write it exactly as printed on your hall ticket, for example 23BSC041.</p>

Building a Disclosure Button with aria-expanded and aria-controls

Sometimes you need more control than <details> gives, for example custom animation or a design team that wants a specific look. Then you build a disclosure: a button that shows and hides a section. Two attributes do the work:

  • aria-expanded tells the user whether the section is open ("true") or closed ("false"). Your script must update it on every click.
  • aria-controls holds the id of the section the button opens. It connects the two.
<style>
  .faq-btn { width: 100%; text-align: left; padding: 10px; font-size: 1rem; }
  .faq-answer { padding: 0 10px; }
</style>

<h3>
  <button type="button" class="faq-btn" aria-expanded="false" aria-controls="ans-1">
    What documents do I need for admission?
  </button>
</h3>
<div id="ans-1" class="faq-answer" hidden>
  <p>Bring your Class 12 marksheet, transfer certificate and two passport size photos.</p>
</div>

<script>
  const btn = document.querySelector('.faq-btn');
  const panel = document.getElementById('ans-1');

  btn.addEventListener('click', () => {
    const isOpen = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', String(!isOpen));
    panel.hidden = isOpen;
  });
</script>

Notice that the button sits inside an <h3>. This keeps the question in the heading outline, so screen reader users can jump from question to question with the heading shortcut. The panel uses the hidden attribute, which hides it from everyone.

Live Regions: Announcing “Seat Booked”

When JavaScript changes text on the page, a sighted user sees it. A screen reader user hears nothing unless the change happens inside a live region. A live region is an element the screen reader watches. When its text changes, the new text is read aloud.

  • role="status" (same as aria-live="polite") waits until the user stops what they are doing, then reads the message. Use it for success messages.
  • role="alert" (same as aria-live="assertive") interrupts at once. Use it only for urgent errors.
<h2>Book a Seat for the Orientation Day</h2>
<button type="button" id="book">Book my seat</button>

<!-- The live region exists from the start, empty. We only change its text. -->
<p id="status" role="status"></p>

<script>
  const statusBox = document.getElementById('status');
  let seatsLeft = 3;

  document.getElementById('book').addEventListener('click', () => {
    if (seatsLeft > 0) {
      seatsLeft = seatsLeft - 1;
      statusBox.textContent = 'Seat booked. ' + seatsLeft + ' seats left.';
    } else {
      statusBox.textContent = 'Sorry, all seats are full.';
    }
  });
</script>

The live region must be in the page before the message arrives. If you create the element and its text at the same moment, many screen readers miss it. So keep an empty <p role="status"> ready and only change its text.

hidden vs aria-hidden

These two look similar but do different things:

AttributeSeen on screen?Read by screen reader?Use it for
hiddenNoNoClosed panels, inactive tabs
aria-hidden="true"YesNoDecorative icons, duplicate text
<!-- hidden: removed for everyone, eyes and screen readers -->
<p hidden>This notice is not shown to anyone right now.</p>

<!-- aria-hidden: still visible, but screen readers skip it -->
<p>
  <span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9734;</span>
  <span>Rated 4 out of 5 by students</span>
</p>

The stars are hidden from the screen reader because “black star, black star...” is noise; the sentence next to them carries the meaning. Never put aria-hidden="true" on something that can receive focus, like a button or link: the keyboard can still reach it, but the screen reader says nothing.

Focus Management Basics

Focus is the place where keyboard input goes. When your script opens or closes something, think about where focus should go next. Two simple rules cover most cases:

  1. When you open a new area, move focus into it, often to its heading.
  2. When you close it, send focus back to the button that opened it.

A heading cannot normally receive focus. Adding tabindex="-1" lets your script focus it with .focus(), without adding it to the Tab order. Never use a positive tabindex like tabindex="5": it breaks the natural order of the page.

<button type="button" id="open-form">Request a callback from the office</button>

<section id="form-area" hidden>
  <h2 id="form-heading" tabindex="-1">Your Details</h2>
  <label for="sname">Student name</label>
  <input id="sname" type="text">
  <button type="button" id="close-form">Close</button>
</section>

<script>
  const openBtn = document.getElementById('open-form');
  const area = document.getElementById('form-area');

  openBtn.addEventListener('click', () => {
    area.hidden = false;
    document.getElementById('form-heading').focus();
  });

  document.getElementById('close-form').addEventListener('click', () => {
    area.hidden = true;
    openBtn.focus();
  });
</script>

Real-Life Example: Accessible Course Tabs

Now fix Ravi’s college website. The course tabs use real buttons with tab roles. Only the selected tab is in the Tab order, and the arrow keys move between tabs. This is the keyboard pattern screen reader users expect from tabs, as described in the ARIA Authoring Practices Guide published by the W3C.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Courses at Sunrise Arts and Science College</title>
  <style>
    [role="tablist"] { display: flex; gap: 4px; }
    [role="tab"] { padding: 8px 14px; border: 1px solid #888; background: #f2f2f2; }
    [role="tab"][aria-selected="true"] { background: #1e6fd9; color: white; }
    [role="tabpanel"] { border: 1px solid #888; padding: 12px; }
  </style>
</head>
<body>
  <h1>Our Courses</h1>

  <div role="tablist" aria-label="Course streams">
    <button type="button" role="tab" id="tab-sci" aria-selected="true" aria-controls="panel-sci">Science</button>
    <button type="button" role="tab" id="tab-com" aria-selected="false" aria-controls="panel-com" tabindex="-1">Commerce</button>
    <button type="button" role="tab" id="tab-arts" aria-selected="false" aria-controls="panel-arts" tabindex="-1">Arts</button>
  </div>

  <div role="tabpanel" id="panel-sci" aria-labelledby="tab-sci">
    <p>B.Sc. Physics, B.Sc. Computer Science, B.Sc. Mathematics.</p>
  </div>
  <div role="tabpanel" id="panel-com" aria-labelledby="tab-com" hidden>
    <p>B.Com General, B.Com Computer Applications.</p>
  </div>
  <div role="tabpanel" id="panel-arts" aria-labelledby="tab-arts" hidden>
    <p>B.A. English, B.A. History, B.A. Economics.</p>
  </div>

  <script>
    const tabs = Array.from(document.querySelectorAll('[role="tab"]'));

    function selectTab(newTab) {
      tabs.forEach((tab) => {
        const selected = tab === newTab;
        tab.setAttribute('aria-selected', String(selected));
        tab.tabIndex = selected ? 0 : -1;
        document.getElementById(tab.getAttribute('aria-controls')).hidden = !selected;
      });
      newTab.focus();
    }

    tabs.forEach((tab, index) => {
      tab.addEventListener('click', () => selectTab(tab));
      tab.addEventListener('keydown', (event) => {
        if (event.key === 'ArrowRight') selectTab(tabs[(index + 1) % tabs.length]);
        if (event.key === 'ArrowLeft') selectTab(tabs[(index - 1 + tabs.length) % tabs.length]);
      });
    });
  </script>
</body>
</html>

Try these changes:

  1. Add a fourth tab for “Vocational” with its own panel. Remember the matching id, aria-controls and aria-labelledby.
  2. Add support for the Home and End keys to jump to the first and last tab.
  3. Remove tabindex="-1" from the Commerce tab and press Tab through the page. Notice the extra stop.

Testing with the Keyboard and a Screen Reader

You do not need special equipment to test. Do this for every component you build:

  1. Keyboard only: put the mouse away. Use Tab and Shift + Tab to move, Enter and Space to press, and arrow keys inside tabs. Can you reach and use everything? Can you always see where focus is?
  2. Screen reader: NVDA is free for Windows. Mac and iPhone have VoiceOver built in (Cmd + F5 on a Mac). Android phones have TalkBack in the accessibility settings. Listen: does each control say its name, its role and its state?
  3. Browser tools: in Chrome DevTools, the Elements panel has an Accessibility tab that shows the role and name of the selected element.

Common Mistakes

  • Adding a role without the behaviour. role="button" on a <span> makes the screen reader say “button”, but Tab skips it and Enter does nothing. Fix: use a real <button>.
  • Forgetting to update the state. Setting aria-expanded="false" once and never changing it lies to the user. Fix: update it in the same function that shows or hides the panel.
  • aria-label on a plain div or span. Many screen readers ignore names on elements that have no role. Fix: put names on interactive elements, landmarks or elements with a proper role.
  • Creating the live region too late. Fix: keep an empty role="status" element in the HTML from the start.

You saw the first mistake in action at the start of this lesson: the fake button that Tab skipped.

Practice Task: An Accessible Hostel FAQ

The hostel FAQ uses a clickable <div>. Rebuild it as a proper disclosure. Your finished version should:

  • Use a real <button> inside an <h3>
  • Have aria-expanded and aria-controls on the button
  • Hide the answer with hidden at the start
  • Announce “Answer shown” or “Answer hidden” through a role="status" element
  • Work fully with Tab and Enter
<h2>Hostel FAQ</h2>

<!-- TODO 1: Turn this div into a real button inside an h3 -->
<!-- TODO 2: Add aria-expanded="false" and aria-controls pointing to the answer -->
<div class="question">Can parents visit on weekends?</div>

<!-- TODO 3: Give the answer an id and hide it at the start -->
<div class="answer">
  <p>Yes. Parents can visit on Saturday and Sunday from 10 AM to 5 PM.</p>
</div>

<!-- TODO 4: Add an empty status message with role="status" -->

<script>
  // TODO 5: On click, flip aria-expanded, show or hide the answer,
  // and write "Answer shown" or "Answer hidden" in the status message.
</script>
Show one possible answer (try it yourself first!)
<h2>Hostel FAQ</h2>

<h3>
  <button type="button" id="q1" aria-expanded="false" aria-controls="a1">
    Can parents visit on weekends?
  </button>
</h3>

<div id="a1" hidden>
  <p>Yes. Parents can visit on Saturday and Sunday from 10 AM to 5 PM.</p>
</div>

<p id="faq-status" role="status"></p>

<script>
  const question = document.getElementById('q1');
  const answerBox = document.getElementById('a1');
  const faqStatus = document.getElementById('faq-status');

  question.addEventListener('click', () => {
    const isOpen = question.getAttribute('aria-expanded') === 'true';
    question.setAttribute('aria-expanded', String(!isOpen));
    answerBox.hidden = isOpen;
    faqStatus.textContent = isOpen ? 'Answer hidden' : 'Answer shown';
  });
</script>

Key Takeaways

  • The first rule of ARIA: use native elements like <button> and <details> before reaching for ARIA.
  • ARIA changes what assistive technology announces. It never adds keyboard behaviour, so you must add that yourself.
  • Give every control a name with visible text, aria-labelledby or aria-label, and extra help with aria-describedby.
  • Keep states like aria-expanded and aria-selected in sync with what is on screen.
  • Use role="status" for messages like “Seat booked”, and move focus sensibly when content opens and closes.
  • Test every component with the keyboard alone and with a real screen reader.

❓ Frequently Asked Questions

No. Most pages built with proper semantic HTML need very little ARIA. You need it mainly for custom widgets like tabs, menus and live messages that HTML has no element for.
Use aria-labelledby when the name already appears as text on the page, because it stays in sync and is translated with the page. Use aria-label only when there is no visible text, such as an icon-only button.
role="status" waits politely and reads the message when the user pauses. role="alert" interrupts immediately, so save it for urgent errors.
On Windows, NVDA is free to download. Macs and iPhones have VoiceOver built in, and Android phones have TalkBack in the accessibility settings.
Yes. All modern browsers support details and summary, including Chrome, Edge, Firefox and Safari on desktop and mobile.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋