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
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.
<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:
.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:
What’s wrong with it:
- Not in the tab order. A
<div>isn’t focusable by default, so keyboard users can’t reach it at all. Addingtabindex="0"fixes the focus problem but creates two more (below). - 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.
- 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. - No
:focus-visiblestyles. Even withtabindex, 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.
<!-- 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
- Icon-only buttons need an accessible name. Use
aria-label="Close"(or visually-hidden text inside the button) — never rely on the icon alone. - Toggle buttons (like a “mute” toggle) should use
aria-pressed="true"/"false"so the state is announced. Don’t swap labels in and out — the user loses the relationship between their action and the new state. - Disabled buttons with the
disabledattribute are removed from the tab order entirely. If you need a “currently unavailable but explained” state, usearia-disabled="true"and keep handling the click (or focus) so you can show a tooltip explaining why. - Loading states should use
aria-busy="true"on the button while the action is pending, and the label should describe what’s happening (“Saving…”). Don’t replace the label with just a spinner.
Checklist
- Uses the native
<button>element - Has
type="button"(unless intentionally submitting a form) - Has a visible, high-contrast focus indicator via
:focus-visible - Hit area is at least 24×24 CSS pixels (44×44 for AAA)
- If icon-only, has an accessible name via
aria-labelor visually-hidden text - If a toggle, communicates state via
aria-pressed