ARIA in Practice · 6 / 8
lesson 6

Common widget patterns

Four patterns you'll meet in every codebase — buttons, custom checkboxes, disclosures, and tabs.

~ 17 min read·lesson 6 of 8
0 / 8

So far we've talked about ARIA at the attribute level. Now let's look at whole patterns — recipes you'll need over and over. Each of these can be done well with a tiny amount of ARIA, or done badly with a lot of it. The bad versions ship constantly. Knowing the canonical shape will save you from reinventing them in a way that ends up harming users.

Buttons that aren't <button>

Sometimes design or framework constraints make a real <button> impractical — usually inside a <a>, <div>, or a custom component. If you must, here is the minimum you owe a fake button:

  1. role="button" so it's announced as a button.
  2. tabindex="0" so the keyboard can reach it.
  3. A keydown handler that activates on both Enter and Space (Space is button-only; pressing Space on a real <a> scrolls the page).
  4. The same focus styles as a real button.
  5. An accessible name (text content or aria-label).
fake-button.html
<div
role="button"
tabindex="0"
onclick="send()"
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); send(); }"
>
Send
</div>

That is more code than <button>Send</button> and easier to break. Rule 1: use the native element. The point of this section is that if you can't, the fake-button pattern is non-negotiably this whole list.

If your "fake button" needs to behave like a toggle (mute on/off, bold/not-bold), add aria-pressed="true" or "false" and update it on every click.

Custom checkboxes

The same rule applies, harder. A real <input type="checkbox"> handles focus, labelling (when paired with <label>), the indeterminate state, the checked state, keyboard activation (Space), and form submission. None of that comes for free with a div.

If a designer insists on a checkbox style that the native input can't easily produce, the modern answer is: keep the native input, hide it visually with .visually-hidden styles, and style the <label> instead. This is the CSS-only custom checkbox pattern, and it's almost always better than reinventing the widget in ARIA.

custom-checkbox.html
<label class="custom-check">
<input type="checkbox" name="newsletter">
<span class="custom-check__box" aria-hidden="true"></span>
<span class="custom-check__label">Subscribe to newsletter</span>
</label>

The <input> provides all the semantics; CSS does the visual work using :checked and :focus-visible selectors on the input to style the sibling <span>.

If for some reason you really cannot use a native input, the ARIA recipe is:

aria-checkbox.html
<div
role="checkbox"
aria-checked="false"
tabindex="0"
onclick="toggle(this)"
onkeydown="if (event.key === ' ') { event.preventDefault(); toggle(this); }"
>
Subscribe to newsletter
</div>

You're now responsible for keeping aria-checked in sync, handling Space (not Enter — checkboxes activate on Space only), styling for keyboard focus, and gathering the value on form submit. This is a lot to maintain. Use the native one.

The disclosure pattern

A disclosure is a button that toggles whether a chunk of content is visible. "Read more," accordion sections, "Show advanced settings." The pattern is small, well-supported, and often reached for incorrectly (people use role="tab" for things that should be disclosures).

Three pieces:

  1. A <button> (a real one) with aria-expanded="true" or "false".
  2. The content panel, identified somehow.
  3. Optionally, aria-controls="panelId" on the button so screen readers know which region the button affects.
disclosure.html
<button aria-expanded="false" aria-controls="advanced">
Show advanced settings
</button>
<div id="advanced" hidden>
... advanced content ...
</div>

When the button is clicked, JavaScript flips aria-expanded between true and false and toggles the hidden attribute on the panel. That's the whole pattern.

Tip

Native HTML has <details> and <summary> for exactly this. They handle aria-expanded, focus, keyboard, and the show/hide automatically. Reach for them first.

aria-controls is one of those attributes screen readers vary on — some announce it, some don't. Set it when the relationship is non-obvious from the layout (the panel is far from the button); skip it when the panel is the next sibling and the connection is visually clear.

role="tab" and tab panels

Tabs look like disclosures but are a different pattern, with stricter keyboard expectations. A tab list is a set of mutually exclusive options — picking one shows its panel, and picking it hides every other panel.

The minimum ARIA shape:

tabs.html
<div role="tablist" aria-label="Account sections">
<button role="tab" aria-selected="true"  aria-controls="p1" id="t1" tabindex="0">Profile</button>
<button role="tab" aria-selected="false" aria-controls="p2" id="t2" tabindex="-1">Billing</button>
</div>

<div role="tabpanel" id="p1" aria-labelledby="t1">...</div>
<div role="tabpanel" id="p2" aria-labelledby="t2" hidden>...</div>

Three things make this pattern hard to fake:

  • Selection follows focus, not Tab. Use tabindex="0" on the active tab and tabindex="-1" on the others. Inside the tablist, arrow keys move between tabs; Tab moves out of the tablist into the panel.
  • aria-selected must update as the user changes tabs.
  • The panel is labelled by its tab via aria-labelledby — that's the link that lets screen-reader users discover what's inside.
ProfileBillingSettings arrow keys move withintab panel — Tab moves here from tablist
Tabs trap arrow keys for in-list navigation. Tab itself jumps out of the tablist into the active panel.

If you can't commit to the keyboard contract above, tabs are the wrong pattern — use disclosures instead, where each section opens independently with no roving focus required.

Watch out

A tablist where every tab is in the Tab order is the most common bug. Sighted users may not notice; keyboard users hit Tab six times to reach the panel. Use roving tabindex.

Try it yourself

check your understanding
You're stuck building a clickable card from <div>. Per rule 1, what's the absolute minimum ARIA + keyboard wiring it needs?
check your understanding
For an accordion section that toggles open and closed, the right pattern is:
check your understanding
In a tablist, which tabindex values are correct?
check your understanding
Which key activates a checkbox role?
check your understanding
You're building a "Mute" toggle that flips between two states. Which attribute reflects the state?
← prevnext lesson →
KeepLearningcertificate
for completing
ARIA in Practice
0 of 8 read