1 minute read

Component Patterns That Don’t Suck

Building the common stuff right

Accordion/Collapse

<div class="accordion">

<h3>

    <button aria-expanded="false" aria-controls="panel1">

      Section 1

    </button>

  </h3>

  <div id="panel1" hidden>

    <p>Content goes here</p>

  </div>

</div>

button.addEventListener('click', () => {

  const expanded = button.getAttribute('aria-expanded') === 'true';

  button.setAttribute('aria-expanded', !expanded);

  panel.hidden = expanded;

});

Tabs That Actually Work

<div role="tablist">

<button role="tab" aria-selected="true" aria-controls="panel1">

    Tab 1

  </button>

  <button role="tab" aria-selected="false" aria-controls="panel2">

    Tab 2

  </button>

</div>

<div role="tabpanel" id="panel1">Content 1</div>

<div role="tabpanel" id="panel2" hidden>Content 2</div>

  Keyboard support required:

  • Arrow keys to move between tabs
  • Home/End for first/last tab
  • Tab key moves into panel content

Dropdown Menus

<nav>

<button aria-expanded="false" aria-haspopup="menu">

    Menu

  </button>

  <ul role="menu" hidden>

    <li role="menuitem"><a href="/home">Home</a></li>

    <li role="menuitem"><a href="/about">About</a></li>

  </ul>

</nav>

Remember: Escape should close, Arrow keys should navigate.

Data Tables That Make Sense

<table>

<caption>Q3 2024 Sales Report</caption>

  <thead>

    <tr>

      <th scope="col">Product</th>

      <th scope="col">Units Sold</th>

      <th scope="col">Revenue</th>

    </tr>

  </thead>

  <tbody>

    <tr>

      <th scope="row">Widgets</th>

      <td>1,234</td>

      <td>$12,340</td>

    </tr>

  </tbody>

</table>

Tables need:

  • <caption> or aria-label for context
  • <th> elements for headers
  • scope attributes for relationships
  • Logical reading order

→ Component patterns that work:

Previous articleNext article
Back to top