Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dylantarre avatar

Aria Patterns

  • 10 installs
  • 37 repo stars
  • Updated December 30, 2025
  • dylantarre/design-system-skills

Provides ARIA roles, states, and properties for building accessible modals, tabs, accordions, and menus.

About

This skill supplies ARIA roles, states, and properties for interactive components. A developer uses it when building custom widgets or fixing screen reader accessibility issues.

  • ARIA roles, states, and properties reference
  • Accessible modal, tab, accordion, and menu patterns

Aria Patterns by the numbers

  • 10 all-time installs (skills.sh)
  • Ranked #1,691 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dylantarre/design-system-skills --skill aria-patterns

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs10
repo stars37
Last updatedDecember 30, 2025
Repositorydylantarre/design-system-skills

What it does

Provides ARIA roles, states, and properties for building accessible modals, tabs, accordions, and menus.

Files

SKILL.mdMarkdownGitHub ↗

ARIA Patterns Guide

Overview

Implement accessible interactive components using correct ARIA roles, states, and properties. Provides copy-paste patterns for common widgets that work with screen readers and keyboard navigation.

When to Use

  • Building custom interactive components
  • Making dynamic content accessible
  • Fixing screen reader issues
  • Adding keyboard support to custom widgets

Quick Reference: First Rules of ARIA

1. Don't use ARIA if native HTML works - <button> over <div role="button"> 2. Don't change native semantics - Don't put role="button" on a heading 3. All interactive ARIA elements must be keyboard accessible 4. Don't use `role="presentation"` or `aria-hidden="true"` on focusable elements 5. All interactive elements must have accessible names

The Process

1. Identify component type: What widget pattern matches? 2. Check native HTML first: Can a semantic element do this? 3. Apply ARIA pattern: Roles, states, properties 4. Add keyboard support: Expected keys for the pattern 5. Test with screen reader: Verify announcements

Component Patterns

Button

Native (preferred):

<button type="button">Click me</button>

Custom (when necessary):

<div
  role="button"
  tabindex="0"
  aria-pressed="false"
  onkeydown="handleKeyDown(event)"
>
  Toggle
</div>

<script>
function handleKeyDown(e) {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    e.target.click();
  }
}
</script>

Toggle Button

<button
  type="button"
  aria-pressed="false"
  onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
>
  <span class="sr-only">Enable</span> Dark Mode
</button>

---

Modal Dialog

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-desc"
>
  <h2 id="modal-title">Confirm Action</h2>
  <p id="modal-desc">Are you sure you want to proceed?</p>

  <button type="button">Cancel</button>
  <button type="button">Confirm</button>
</div>

Required behavior:

  • Focus moves to dialog on open
  • Focus trapped within dialog
  • Escape key closes dialog
  • Focus returns to trigger on close
// Focus trap example
function trapFocus(dialog) {
  const focusable = dialog.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  dialog.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  });
}

---

Dropdown Menu

<div class="dropdown">
  <button
    type="button"
    aria-haspopup="menu"
    aria-expanded="false"
    aria-controls="dropdown-menu"
    id="dropdown-trigger"
  >
    Options
  </button>

  <ul
    role="menu"
    id="dropdown-menu"
    aria-labelledby="dropdown-trigger"
    hidden
  >
    <li role="none">
      <button role="menuitem" tabindex="-1">Edit</button>
    </li>
    <li role="none">
      <button role="menuitem" tabindex="-1">Duplicate</button>
    </li>
    <li role="none">
      <button role="menuitem" tabindex="-1">Delete</button>
    </li>
  </ul>
</div>

Keyboard:

  • Enter/Space: Open menu, activate item
  • Arrow Down: Next item (or first if closed)
  • Arrow Up: Previous item
  • Escape: Close menu
  • Home: First item
  • End: Last item

---

Tabs

<div class="tabs">
  <div role="tablist" aria-label="Account settings">
    <button
      role="tab"
      aria-selected="true"
      aria-controls="panel-1"
      id="tab-1"
      tabindex="0"
    >
      Profile
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-2"
      id="tab-2"
      tabindex="-1"
    >
      Security
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-3"
      id="tab-3"
      tabindex="-1"
    >
      Billing
    </button>
  </div>

  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
    Profile content...
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden>
    Security content...
  </div>
  <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden>
    Billing content...
  </div>
</div>

Keyboard:

  • Arrow Left/Right: Move between tabs
  • Home: First tab
  • End: Last tab
  • Tab: Move into panel content

---

Accordion

<div class="accordion">
  <h3>
    <button
      type="button"
      aria-expanded="true"
      aria-controls="section-1"
      id="accordion-header-1"
    >
      Section 1
    </button>
  </h3>
  <div
    role="region"
    id="section-1"
    aria-labelledby="accordion-header-1"
  >
    Section 1 content...
  </div>

  <h3>
    <button
      type="button"
      aria-expanded="false"
      aria-controls="section-2"
      id="accordion-header-2"
    >
      Section 2
    </button>
  </h3>
  <div
    role="region"
    id="section-2"
    aria-labelledby="accordion-header-2"
    hidden
  >
    Section 2 content...
  </div>
</div>

---

Tooltip

<button
  type="button"
  aria-describedby="tooltip-1"
>
  Help
</button>

<div
  role="tooltip"
  id="tooltip-1"
  hidden
>
  Click here for more information
</div>

Note: For interactive content, use a disclosure or dialog instead.

---

Alert / Status Messages

<!-- Alert (important, interruptive) -->
<div role="alert">
  Error: Please enter a valid email address.
</div>

<!-- Status (polite update) -->
<div role="status" aria-live="polite">
  3 items in cart
</div>

<!-- Live region (for dynamic content) -->
<div aria-live="polite" aria-atomic="true">
  <!-- Content updates announced to screen readers -->
</div>

---

Combobox (Autocomplete)

<div class="combobox">
  <label for="search-input">Search</label>
  <input
    type="text"
    id="search-input"
    role="combobox"
    aria-autocomplete="list"
    aria-expanded="false"
    aria-controls="search-listbox"
    aria-activedescendant=""
  >

  <ul
    role="listbox"
    id="search-listbox"
    hidden
  >
    <li role="option" id="option-1">Option 1</li>
    <li role="option" id="option-2">Option 2</li>
    <li role="option" id="option-3" aria-selected="true">Option 3</li>
  </ul>
</div>

Update `aria-activedescendant` to the ID of the highlighted option.

---

Progress / Loading

<!-- Determinate progress -->
<div
  role="progressbar"
  aria-valuenow="75"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Upload progress"
>
  75%
</div>

<!-- Indeterminate loading -->
<div
  role="status"
  aria-label="Loading"
>
  <span class="spinner" aria-hidden="true"></span>
  <span class="sr-only">Loading...</span>
</div>

---

Common ARIA Attributes

AttributePurposeExample
aria-labelAccessible namearia-label="Close"
aria-labelledbyName from elementaria-labelledby="heading-1"
aria-describedbyDescriptionaria-describedby="hint-1"
aria-expandedOpen/closed statearia-expanded="true"
aria-controlsControlled elementaria-controls="menu-1"
aria-hiddenHide from ATaria-hidden="true"
aria-liveAnnounce updatesaria-live="polite"
aria-pressedToggle statearia-pressed="false"
aria-selectedSelection statearia-selected="true"
aria-currentCurrent itemaria-current="page"

Screen Reader Only Text

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Testing

1. Keyboard only: Tab through, use arrows, Enter, Escape 2. Screen reader: Test with VoiceOver (Mac), NVDA (Windows), or JAWS 3. Check announcements: Are labels, states, and changes announced? 4. axe DevTools: Run automated accessibility audit

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.