Patterns Button

Button

A control that triggers an action. The bedrock pattern — get this wrong and assistive tech can’t reach the rest of your interface.

Reviewed against the methodology checklist Updated

Try the keyboard

When to use

Use a <button> whenever activating the control performs an action on the current page — submit a form, open a dialog, toggle a section, copy text. If the action is “go somewhere else”, you want a link, not a button.

The pattern

Accessible button

The native <button> element gives you everything for free: it’s focusable, announced as a button by every screen reader, activated by both Enter and Space, and inherits a visible focus ring from the browser (which we then style). Use type="button" unless it’s submitting a form, otherwise it defaults to type="submit" and will fire form submission unexpectedly.

HTML
<button type="button">Subscribe</button>

That’s it. No JavaScript, no ARIA, no tabindex.

Styling without breaking it

Browser-default buttons are ugly, so most projects style them. The trap is that outline: none is a popular choice — and it removes the focus ring. Always replace it with something visible:

CSS
.button {
display: inline-flex;
align-items: center;
padding-inline: 1rem;
padding-block: 0.5rem;
min-height: 2.75rem; /* WCAG 2.5.8 target size */
background-color: #6a2a8a;
color: #ffffff;
border: 2px solid transparent;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
}

.button:focus-visible {
outline: 3px solid #ff6b00;
outline-offset: 2px;
}

Use :focus-visible rather than :focus so the ring only shows for keyboard users — mouse clicks won’t trigger it. (If you want to be extra safe, fall back to :focus for browsers without :focus-visible support, but that’s a tiny minority now.)

Anti-pattern: the <div> that pretends

Every codebase has one. A designer hands you a button-shaped thing, the framework component you reach for is “Box”, you slap an onClick on it, and you ship. Here it is — looks the same, fails four ways:

Don’t do this
Subscribe

What’s wrong with it:

  1. Not in the tab order. A <div> isn’t focusable by default, so keyboard users can’t reach it at all. Adding tabindex="0" fixes the focus problem but creates two more (below).
  2. No role announced. Screen readers say the text content but don’t announce that it’s a button. The user doesn’t know they can activate it.
  3. Space doesn’t activate it. Screen-reader users typically press Space on buttons. With a <div>, Space scrolls the page instead — the user has no idea their key did anything to the control.
  4. No :focus-visible styles. Even with tabindex, you have to remember to style the focus ring. Native <button> gets one for free.

Fixing all of this with ARIA + a keyboard handler is a dozen lines of code. Fixing it by typing <button> is zero.

HTML
<!-- The "fixed" div needs all of this just to match a real button -->
<div
role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleClick(); }"
>
Subscribe
</div>

Common variations

Checklist

Screen reader transcript

Voiced by George — reading what VoiceOver on macOS Safari announces. Other readers (NVDA, JAWS) phrase things differently; the meaning is what matters.

Screen reader announcements for each step
ListenYou doScreen reader says
Tab onto the buttonSubscribe, button
Press Enter(button activates — your handler runs)

Screen reader transcript — anti-pattern

Same interaction, but on the broken version above. Notice what the user is missing.

Screen reader announcements for each step
ListenYou doScreen reader says
Tab through the page(focus skips the fake button entirely)
Reach the fake button by pressing Tab repeatedly with tabindex setSubscribe (no role announced — sounds like static text)
Press Space to activate(page scrolls instead of activating the button)

WCAG references