# ARIA Practical Guide: Implementing Accessible Web Interfaces

> How do we apply ARIA in real projects? This guide shows when to use it and how to use it effectively, with live regions, custom widgets, and modal patterns.

**Published:** 2026-01-22 | **Updated:** 2026-01-22

---


Developers often make the same mistake after learning ARIA. They understand the concept, but they are unsure when and how to apply it in real projects.

{{< img src="images/contents/og-aria-practical-guide-main.png" alt="ARIA practical guide main visual with ARIA attributes highlighted in a code editor" caption="Cover image example: visual that symbolizes applying ARIA attributes · Generated by Nanobanana AI" >}}

You may have heard the phrase: "ARIA is a last resort." Use semantic HTML first, and add ARIA only when native HTML is not enough. This guide follows that principle and shows how to use ARIA effectively in real work.

## 1. The Golden Rule of ARIA: When to Use It, When Not to

### 1.1 When You Should NOT Use ARIA

Many developers overuse ARIA. Start by checking cases where ARIA is unnecessary.

{{< img src="images/contents/aria-dos-and-donts.png" alt="Diagram comparing ARIA mistakes and recommended practices - incorrect on the left, correct on the right" caption="Before/after comparison of ARIA usage · Generated by Nanobanana AI" >}}

```html
<!-- ❌ Bad: Unnecessary ARIA -->
<button role="button" aria-label="Click">Click</button>
<!-- <button> is already a button; role is redundant -->

<!-- ❌ Bad: ARIA used when semantic HTML exists -->
<div role="heading" aria-level="1">Title</div>
<!-- Use <h1>Title</h1> instead -->

<!-- ❌ Bad: aria-label when visible text already exists -->
<button aria-label="Save">
  <i class="icon-save"></i> Save
</button>
<!-- If visible text exists, aria-label is unnecessary -->

<!-- ✅ Good: No ARIA needed (semantic HTML is enough) -->
<button>Save</button>
<h1>Title</h1>
<form>
  <label for="email">Email</label>
  <input id="email" type="email">
</form>
```

**Elements that are sufficient without ARIA:**
- Button: `<button>`
- Link: `<a>`
- Form: `<form>`, `<input>`, `<textarea>`, `<select>`
- Headings: `<h1>` ~ `<h6>`
- Navigation: `<nav>`
- Main: `<main>`
- Article: `<article>`
- Section: `<section>`

### 1.2 When You SHOULD Use ARIA

ARIA is needed in cases like these:

1. **No semantic HTML exists**: custom widgets or special UI
2. **Provide extra information**: native HTML is not enough
3. **Announce state changes**: dynamic updates for screen readers

```html
<!-- ✅ Case 1: Custom widget -->
<div class="custom-tabs">
  <div role="tablist">
    <button role="tab" aria-selected="true" aria-controls="panel-1" tabindex="0">
      Tab 1
    </button>
    <button role="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1">
      Tab 2
    </button>
  </div>
  <div id="panel-1" role="tabpanel">Content 1</div>
  <div id="panel-2" role="tabpanel" hidden>Content 2</div>
</div>

<!-- ✅ Case 2: Additional info -->
<button aria-label="Open chart">📊</button>
<!-- Icon-only button requires aria-label -->

<!-- ✅ Case 3: Announce state change -->
<span aria-live="polite" aria-atomic="true" id="notifications"></span>
<script>
  // Add message after user action
  document.getElementById('notifications').textContent = 'Saved.';
</script>
```

## 2. Practical ARIA Patterns

{{< img src="images/contents/aria-roles-hierarchy.png" alt="Diagram showing ARIA role hierarchy: widget roles, live region roles, and structure roles" caption="Hierarchy of ARIA role types · Generated by Nanobanana AI" >}}

### 2.1 Labeling: aria-label vs aria-labelledby

These attributes tell assistive technologies **what an element is**.
- `aria-label` is best when you need a **short, direct label** (icon buttons).
- `aria-labelledby` is best when you can **reference existing visible text**, keeping visual and spoken labels aligned.

```html
<!-- 1. aria-label: simple label -->
<button aria-label="Close">×</button>

<!-- 2. aria-labelledby: reference another element -->
<h2 id="dialog-title">User Settings</h2>
<div role="dialog" aria-labelledby="dialog-title">
  <!-- Dialog content -->
</div>

<!-- 3. Combine labels from multiple elements -->
<span id="search-label">Product search</span>
<span id="search-hint">(name, category)</span>
<input
  aria-labelledby="search-label search-hint"
  placeholder="Search"
>

<!-- 4. Visually hidden label -->
<style>
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    border: 0;
  }
</style>

<button>
  <span class="sr-only">Back</span>
  ←
</button>
```

### 2.2 Descriptions and Hints: aria-describedby

`aria-describedby` links **supplementary information**. Use it for hints, error messages, and extra context that should be read **after** the main label. Clear separation between label and description keeps information structured.

```html
<!-- Additional help text -->
<label for="password">Password</label>
<input
  id="password"
  type="password"
  aria-describedby="pwd-requirements pwd-strength"
>
<div id="pwd-requirements" class="hint">
  Minimum 8 chars, include uppercase, numbers, and symbols (@, #, $)
</div>
<div id="pwd-strength" aria-live="polite">
  Strength: Weak
</div>

<!-- Complex image description -->
<img
  src="chart.png"
  alt="Monthly sales"
  aria-describedby="chart-desc"
>
<p id="chart-desc">
  Sales from January to December 2024.
  Peak in July at 15,000 units, lowest in February at 5,000.
</p>

<!-- Error message -->
<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error">
<span id="email-error" role="alert">
  Please enter a valid email address.
</span>
```

### 2.3 States: aria-checked, aria-pressed, aria-current

These attributes communicate **current UI state** to assistive technologies. The goal is to mirror what sighted users see (checked, pressed, current location, expanded). Native elements expose state automatically, so you usually **manage these only for custom widgets**.

```html
<!-- 1. Checkbox/radio state -->
<label>
  <input type="checkbox">
  I agree to the terms
</label>

<!-- Native checkbox/radio already exposes state.
     Use aria-checked only for custom widgets. -->

<!-- 2. Toggle button state -->
<button
  aria-pressed="false"
  aria-label="Mute"
  id="mute-btn"
>
  🔊
</button>

<script>
  const muteBtn = document.getElementById('mute-btn');
  muteBtn.addEventListener('click', function() {
    const isPressed = this.getAttribute('aria-pressed') === 'true';
    this.setAttribute('aria-pressed', !isPressed);
    // Toggle mute logic
  });
</script>

<!-- 3. Current page in navigation -->
<nav>
  <a href="/products">Products</a>
  <a href="/blog" aria-current="page">Blog</a>
  <a href="/about">About</a>
</nav>

<!-- 4. Expand/collapse state -->
<button aria-expanded="false" aria-controls="submenu">
  Categories
</button>
<ul id="submenu" hidden>
  <li><a href="#">Submenu 1</a></li>
  <li><a href="#">Submenu 2</a></li>
</ul>

<script>
  document.querySelector('button').addEventListener('click', function() {
    const isExpanded = this.getAttribute('aria-expanded') === 'true';
    this.setAttribute('aria-expanded', !isExpanded);
    document.getElementById('submenu').hidden = isExpanded;
  });
</script>
```

**Quick summary**
- `aria-checked`: expresses a selection/checked state. Use it for **custom** checkboxes, radios, and switches.
- `aria-pressed`: expresses **on/off** for toggle buttons.
- `aria-current`: indicates the **current location**, most often the current page in navigation.
- `aria-expanded`: communicates **expanded/collapsed** state for accordions, dropdowns, trees, etc.

## 3. Live Regions: Announcing Dynamic Updates

Live regions inform screen reader users when content changes without a page refresh.

{{< img src="images/contents/aria-live-regions.png" alt="Diagram visualizing aria-live for real-time updates and dynamic alerts" caption="Live region announcement flow · Generated by Nanobanana AI" >}}

### 3.1 aria-live

`aria-live` marks regions that should be **announced automatically when they change**. It’s essential for dynamic updates such as save confirmations, errors, or live search results. Use it **only where necessary** to avoid interrupting users too often.

```html
<!-- 1. aria-live="polite" (default) -->
<!-- Announces after the user finishes the current task -->
<div id="notifications" aria-live="polite">
  <!-- Messages appear here -->
</div>

<!-- 2. aria-live="assertive" -->
<!-- Announces immediately (use for urgent alerts only) -->
<div id="error-alert" aria-live="assertive">
  <!-- Immediate error message -->
</div>

<!-- 3. aria-atomic="true" -->
<!-- Announces the whole region, not just the changed part -->
<div aria-live="polite" aria-atomic="true" id="search-results">
  <p>Results: 23</p>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</div>

<!-- 4. aria-relevant="additions text" -->
<!-- Control which changes are announced -->
<div aria-live="polite" aria-relevant="additions text">
  <!-- Only additions and text changes announced -->
</div>
```

**Tip**: Use `aria-live="assertive"` only for urgent alerts (errors, security warnings). For most status updates, keep it `polite` to avoid interrupting users.

### 3.2 Example: Live Search

Live search updates are obvious visually, but screen reader users won’t know results changed unless you announce them. Using `role="status"` or `aria-live` ensures they receive the same feedback.

{{< img src="images/contents/aria-live-search-example.png" alt="Example showing aria-live announcing search results in a live search UI" caption="Live search announcement example · Generated by Nanobanana AI" >}}

```html
<div class="search-container">
  <input
    id="search-input"
    type="text"
    placeholder="Search..."
    aria-describedby="search-help"
  >
  <span id="search-help" class="sr-only">
    Results update as you type.
  </span>
</div>

<!-- Live region for search results -->
<div
  id="search-results"
  role="status"
>
  <!-- Results appear here -->
</div>

<script>
  const input = document.getElementById('search-input');
  const resultsDiv = document.getElementById('search-results');

  input.addEventListener('input', function(e) {
    const query = e.target.value.trim();

    if (!query) {
      resultsDiv.innerHTML = '';
      return;
    }

    // API request (example)
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => {
        resultsDiv.innerHTML = `
          <p>
            Found ${data.count} results.
          </p>
          <ul>
            ${data.items.map(item => `<li>${item.name}</li>`).join('')}
          </ul>
        `;
      });
  });
</script>
```

## 4. Custom Widget Implementations

Use ARIA when semantic HTML alone cannot cover complex UI.

{{< img src="images/contents/aria-widget-patterns.png" alt="Diagram showing custom widget patterns: tabs, accordion, dropdown, modal with ARIA" caption="Summary of widget patterns (tabs/accordion/dropdown/modal) · Generated by Nanobanana AI" >}}

### 4.1 Tabs

Tabs are a classic **custom widget**. The `tablist/tab/tabpanel` roles define structure, and **keyboard rules** (arrow keys, Home/End) are required for full accessibility.

{{< img src="images/contents/aria-tabs-widget-implementation.png" alt="Diagram explaining ARIA structure and keyboard navigation for a tabs widget" caption="Tabs widget: role/aria wiring and keyboard flow · Generated by Nanobanana AI" >}}

```html
<div class="tabs">
  <div role="tablist">
    <button
      role="tab"
      aria-selected="true"
      aria-controls="panel-1"
      tabindex="0"
      id="tab-1"
    >
      Basic Info
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-2"
      tabindex="-1"
      id="tab-2"
    >
      Details
    </button>
  </div>

  <div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
    Basic info content
  </div>
  <div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
    Detailed content
  </div>
</div>

<style>
  [role="tab"][aria-selected="false"] {
    opacity: 0.6;
  }
</style>

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

  tabs.forEach(tab => {
    tab.addEventListener('click', function() {
      // Deactivate all tabs
      tabs.forEach(t => {
        t.setAttribute('aria-selected', 'false');
        t.setAttribute('tabindex', '-1');
      });
      panels.forEach(p => p.hidden = true);

      // Activate clicked tab
      this.setAttribute('aria-selected', 'true');
      this.setAttribute('tabindex', '0');
      const panelId = this.getAttribute('aria-controls');
      document.getElementById(panelId).hidden = false;
    });

    // Keyboard navigation
    tab.addEventListener('keydown', function(e) {
      let nextTab;
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
        nextTab = this.nextElementSibling || tabs[0];
      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
        nextTab = this.previousElementSibling || tabs[tabs.length - 1];
      } else if (e.key === 'Home') {
        nextTab = tabs[0];
      } else if (e.key === 'End') {
        nextTab = tabs[tabs.length - 1];
      }

      if (nextTab) {
        e.preventDefault();
        nextTab.click();
        nextTab.setAttribute('tabindex', '0');
        this.setAttribute('tabindex', '-1');
        nextTab.focus();
      }
    });
  });
</script>
```

### 4.2 Accordion

Accordions rely on clear **expanded/collapsed state**. Expose it with `aria-expanded`, and link headers to panels with `role="region"` and `aria-labelledby` so screen readers understand the structure.

```html
<div class="accordion">
  <div class="accordion-item">
    <button
      aria-expanded="false"
      aria-controls="section-1"
      id="accordion-1"
      class="accordion-btn"
    >
      FAQ 1
    </button>
    <div
      id="section-1"
      role="region"
      aria-labelledby="accordion-1"
      hidden
      class="accordion-content"
    >
      <p>Answer 1</p>
    </div>
  </div>

  <div class="accordion-item">
    <button
      aria-expanded="false"
      aria-controls="section-2"
      id="accordion-2"
      class="accordion-btn"
    >
      FAQ 2
    </button>
    <div
      id="section-2"
      role="region"
      aria-labelledby="accordion-2"
      hidden
      class="accordion-content"
    >
      <p>Answer 2</p>
    </div>
  </div>
</div>

<style>
  .accordion-content {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
  }

  .accordion-content:not([hidden]) {
    max-height: 500px;
  }

  button[aria-expanded="true"]::after {
    content: '▼';
  }

  button[aria-expanded="false"]::after {
    content: '▶';
  }
</style>

<script>
  document.querySelectorAll('.accordion-btn').forEach(btn => {
    btn.addEventListener('click', function() {
      const isExpanded = this.getAttribute('aria-expanded') === 'true';
      this.setAttribute('aria-expanded', !isExpanded);
      document.getElementById(this.getAttribute('aria-controls')).hidden = isExpanded;
    });
  });
</script>
```

### 4.3 Dropdown Menu

Dropdowns are typically a **toggle button plus a list**. For most navigation use cases, a simple `button + ul` is the safest pattern. Use complex `menu` roles only when the interaction truly matches application menus.

```html
<div class="dropdown">
  <button
    id="menu-btn"
    aria-expanded="false"
    aria-controls="menu-list"
  >
    Menu
  </button>

  <ul id="menu-list" hidden aria-labelledby="menu-btn">
    <li><a href="#">Option 1</a></li>
    <li><a href="#">Option 2</a></li>
    <li><a href="#">Option 3</a></li>
  </ul>
</div>

<script>
  const btn = document.getElementById('menu-btn');
  const menu = document.getElementById('menu-list');
  const items = menu.querySelectorAll('a');

  btn.addEventListener('click', () => {
    const isExpanded = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', !isExpanded);
    menu.hidden = isExpanded;

    if (!isExpanded) {
      items[0].focus();
    }
  });

  // Close on Escape
  menu.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      btn.click();
      btn.focus();
    }
  });

  // Close on outside click
  document.addEventListener('click', (e) => {
    if (!e.target.closest('.dropdown')) {
      menu.hidden = true;
      btn.setAttribute('aria-expanded', 'false');
    }
  });
</script>
```

## 5. Modal/Dialog Pattern

Modals are all about **focus management**. Move focus into the dialog on open, and restore it on close. `aria-modal="true"` tells assistive tech that the dialog is the active interaction context.

{{< img src="images/contents/aria-modal-focus-trap.png" alt="Diagram showing focus management in a modal dialog, including focus movement and aria-modal" caption="Modal focus management flow (open/close/restore) · Generated by Nanobanana AI" >}}

```html
<button id="open-modal">Open modal</button>

<div
  id="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  hidden
>
  <div class="modal-content">
    <h2 id="modal-title">Important Info</h2>

    <p>This is modal content.</p>

    <button id="close-modal">Close</button>
  </div>
</div>

<style>
  #modal:not([hidden]) {
    display: flex;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
  }

  .modal-content {
    margin: auto;
    background: white;
    padding: 2rem;
    border-radius: 8px;
  }
</style>

<script>
  const modal = document.getElementById('modal');
  const openBtn = document.getElementById('open-modal');
  const closeBtn = document.getElementById('close-modal');
  let previousFocus = null;

  openBtn.addEventListener('click', () => {
    previousFocus = document.activeElement;
    modal.hidden = false;
    // Inert everything except the modal
    const siblings = Array.from(document.body.children).filter(el => el !== modal);
    siblings.forEach(el => (el.inert = true));

    // Move focus into the modal
    const focusableElements = modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    focusableElements[0]?.focus();

    // Focus trap inside modal
    focusableElements[focusableElements.length - 1].addEventListener('keydown', (e) => {
      if (e.key === 'Tab' && !e.shiftKey) {
        e.preventDefault();
        focusableElements[0].focus();
      }
    });

    focusableElements[0].addEventListener('keydown', (e) => {
      if (e.key === 'Tab' && e.shiftKey) {
        e.preventDefault();
        focusableElements[focusableElements.length - 1].focus();
      }
    });
  });

  closeBtn.addEventListener('click', () => {
    modal.hidden = true;
    const siblings = Array.from(document.body.children).filter(el => el !== modal);
    siblings.forEach(el => (el.inert = false));
    previousFocus?.focus();
  });

  // Close on Escape
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !modal.hidden) {
      closeBtn.click();
    }
  });
</script>
```

## 6. ARIA Validation

### 6.1 Common ARIA Mistakes

ARIA is powerful, but **misuse can reduce accessibility**. These are the most common real-world pitfalls to watch for during implementation and review.

```html
<!-- ❌ Mistake 1: Misusing roles -->
<div role="button" onclick="alert('Click')">Click me</div>
<!-- Issue: no keyboard focus, no keyboard activation -->

<!-- ✅ Correct -->
<button onclick="alert('Click')">Click me</button>

<!-- ❌ Mistake 2: aria-label without a visible label -->
<input aria-label="Search" placeholder="Search">
<!-- Issue: visible users see no label, assistive tech gets a label -->

<!-- ✅ Correct -->
<label for="search">Search</label>
<input id="search" placeholder="Search">

<!-- ❌ Mistake 3: Overusing live regions -->
<div aria-live="assertive" id="log">
  Log messages...
</div>
<!-- Issue: announces everything, can be noisy -->

<!-- ✅ Correct -->
<div aria-live="polite" aria-atomic="false" id="notification">
  <!-- Only latest message -->
</div>

<!-- ❌ Mistake 4: Inconsistent tabindex in tabs -->
<div role="tablist">
  <div role="tab" tabindex="0">First</div>
  <div role="tab">Second</div> <!-- missing tabindex -->
  <div role="tab" tabindex="0">Third</div>
</div>
<!-- Issue: inconsistent focus order -->

<!-- ✅ Correct: only first tab is 0, others are -1 -->
<div role="tablist">
  <div role="tab" tabindex="0">First</div>
  <div role="tab" tabindex="-1">Second</div>
  <div role="tab" tabindex="-1">Third</div>
</div>
```

### 6.2 ARIA Validation Checklist

**When to use ARIA**
- [ ] Is semantic HTML insufficient?
- [ ] Is this a custom widget?
- [ ] Is this information important for screen readers?

**When using ARIA**
- [ ] Did you set role, aria-label/labelledby, aria-describedby?
- [ ] Does keyboard navigation work?
- [ ] Do aria-* states reflect changes?
- [ ] Is focus managed properly? (e.g., on modal open)
- [ ] Are live regions used sparingly?

**Testing**
- [ ] Screen readers (NVDA, JAWS, VoiceOver)
- [ ] Keyboard-only navigation
- [ ] axe DevTools, Lighthouse
- [ ] Compare to WAI-ARIA Authoring Practices

## 7. ARIA Tips and Best Practices

{{< img src="images/contents/aria-keyboard-navigation-patterns.png" alt="Guide summarizing keyboard navigation patterns for custom widgets such as tabs, menus, accordion, and dropdown" caption="Keyboard shortcuts by widget type · Generated by Nanobanana AI" >}}

### 7.1 Role Tips

Roles add meaning to elements that lack semantics. Avoid duplicating roles on native elements; use roles **only when needed**.

```html
<!-- 1. Avoid duplicating roles on native elements -->
<button>Good</button>
<div role="button">Use only when needed</div>

<!-- 2. Prefer changing elements over changing roles -->
<!-- ❌ Not good -->
<div id="button" role="button">Click</div>
<script>
  document.getElementById('button').setAttribute('role', 'link');
</script>

<!-- ✅ Good -->
<button>Click</button>

<!-- 3. Explicitly define composite roles -->
<div role="tablist">
  <div role="tab">Tab 1</div>
  <div role="tabpanel">Content 1</div>
</div>
```

### 7.2 ARIA and CSS

Styling based on ARIA state keeps **visual feedback aligned with accessibility state**. For example, changing color when `aria-pressed="true"` makes the UI consistent for everyone.

```css
/* Style based on ARIA state */
button[aria-pressed="true"] {
  background-color: #4A90E2;
  color: white;
}

button[aria-pressed="false"] {
  background-color: #f0f0f0;
  color: #333;
}

/* Focus indicators */
[role="button"]:focus {
  outline: 3px solid #4A90E2;
  outline-offset: 2px;
}

/* Optional: visualize live regions */
[aria-live="polite"] {
  border-left: 3px solid #27AE60;
  padding-left: 0.5rem;
}

/* Hidden panels */
[role="tabpanel"][hidden] {
  display: none;
}
```

### 7.3 Performance Considerations

Live regions can be noisy and expensive if overused. Batch updates where possible and keep only the latest status to reduce cognitive and performance overhead.

```javascript
// ❌ Bad: append each update individually
const notifications = document.getElementById('notifications');
for (let i = 0; i < 1000; i++) {
  notifications.innerHTML += `<p>Notification ${i}</p>`;
}

// ✅ Good: batch updates
const notifications = document.getElementById('notifications');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const p = document.createElement('p');
  p.textContent = `Notification ${i}`;
  fragment.appendChild(p);
}
notifications.appendChild(fragment);

// ✅ Good: keep only the latest message
const notification = document.getElementById('notification');
function showNotification(message) {
  notification.innerHTML = message;
  setTimeout(() => {
    notification.innerHTML = '';
  }, 5000);
}
```

## 8. ARIA in Real Projects

### 8.1 Libraries and Frameworks

Many modern frameworks include ARIA support:

```javascript
// React example
function Tabs() {
  const [selected, setSelected] = useState(0);

  return (
    <div role="tablist">
      {tabs.map((tab, i) => (
        <button
          key={i}
          role="tab"
          aria-selected={i === selected}
          aria-controls={`panel-${i}`}
          onClick={() => setSelected(i)}
        >
          {tab.label}
        </button>
      ))}
      {tabs.map((tab, i) => (
        <div
          key={i}
          id={`panel-${i}`}
          role="tabpanel"
          aria-labelledby={`tab-${i}`}
          hidden={i !== selected}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}
```

### 8.2 ARIA Pattern Library

The [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) is the official pattern library. Use proven patterns instead of reinventing the wheel:

- Buttons, links
- Form inputs
- Navigation
- Tabs, accordions, menus
- Dialogs, alerts
- Grids, tables

## Conclusion

ARIA is powerful, but it must be used responsibly. Remember:

1. **Use semantic HTML first**: leverage native elements
2. **Add ARIA only when needed**: keep it minimal
3. **Keyboard navigation is required**: everything must work without a mouse
4. **Test with screen readers**: verify real experience
5. **Manage focus and state**: communicate dynamic changes

Accessible interfaces improve the experience for everyone. When you use ARIA correctly, both assistive tech users and all other users benefit.


---

{{< faq >}}

## Other Posts in This Series

- [Keyboard Accessibility A to Z: Building Websites Everyone Can Use Without a Mouse]({{< relref "/posts/keyboard-accessibility-a-to-z" >}})
- [Color Accessibility: Designing Colors That Everyone Can Perceive]({{< relref "/posts/color-accessibility" >}})
- [Form Accessibility Mastery: Designing Accessible Input Forms for Everyone]({{< relref "/posts/form-accessibility-mastery" >}})

