Skip to main content

Accessible Buttons

Buttons are how people do things on the web: send a message, add to a basket, open a menu, save a draft. Because they are so common, they are also where a lot of accessibility problems hide, usually because someone built one out of a div.

The HTML button element does a huge amount of work for you. It can be reached with Tab, activated with Enter and Space, announced as a "button," used with voice control, and styled as you like. Use it, name it well, and add a little ARIA only when a button has a state, such as "on" or "expanded."


What You Will Learn​

  • Why the real button element beats a div every time
  • The type attribute, and the form-submit trap
  • How to name buttons, including icon-only ones
  • Toggle and disclosure buttons (aria-pressed, aria-expanded)
  • Disabled buttons, aria-disabled, and loading states
  • How big and how visible a button should be
  • How to check your buttons

The Big Idea​

index.html
<!-- A normal action -->
<button type="button">Save recipe</button>

<!-- A form submit -->
<form>
<label for="email">Email</label>
<input id="email" type="email">
<button type="submit">Subscribe</button>
</form>

<!-- An icon-only button needs a name -->
<button type="button" aria-label="Delete recipe">
<span aria-hidden="true">πŸ—‘</span>
</button>

<!-- A toggle button: the label stays, the state changes -->
<button type="button" aria-pressed="false">Mute</button>

<!-- A disclosure button: it shows or hides something -->
<button type="button" aria-expanded="false" aria-controls="details">
Delivery details
</button>
<div id="details" hidden>Free delivery on orders over 20 dollars.</div>

Try both buttons in the lab, with the mouse and then with Enter and Space. Then tick the box at the top, and listen to what happens to the spoken line.

Why Use a Real button?​

A native button gives you all of this for free:

You getWhat it means
FocusIt is in the Tab order, with a focus ring.
KeyboardEnter and Space both activate it.
RoleScreen readers say "button."
NameThe text inside becomes its accessible name.
Statedisabled is understood by browsers and assistive technology.
FormsIt can submit, reset, or do nothing inside a form.
Voice controlPeople can say "click Save."

A div has none of these. To rebuild them you need role="button", tabindex="0", handlers for both Enter and Space (with preventDefault on Space, so the page does not scroll), a focus style, and disabled behaviour. That is a lot of code to get worse results.

<!-- Last resort only. Prefer a real button. -->
<div role="button" tabindex="0" id="fake">Save</div>

<script>
const fake = document.getElementById("fake");
fake.addEventListener("click", save);
fake.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
save();
}
});
</script>
warning

Even with all that code, a fake button still misses form behaviour, disabled support, and a few small details that vary between browsers and assistive technologies. If you can use <button>, use it.

The type Attribute​

A button inside a form is a submit button by default. That is a classic source of bugs: a "Show password" button that unexpectedly submits the form.

TypeWhat it does
type="submit"Submits the form (the default inside a form).
type="button"Does nothing by itself. Use it for actions run by script.
type="reset"Resets the form fields. Rarely a good idea.

Always write the type. Use type="submit" for the one real submit button, and type="button" for everything else.

There is also input type="submit", input type="button", and input type="image". They work, but button is more flexible because it can hold text, icons, and other content.

Naming Buttons​

A button's name comes from its content, so a visible text label is the best choice. See Accessible Names for the full rules.

  • Use a verb or action: "Save recipe," "Add to basket," "Send message."
  • Be specific. If there are several "Delete" buttons on a page, make each one unique: "Delete sourdough recipe."
  • Do not repeat the role: "Save," not "Save button."
  • Icon-only buttons need a name with aria-label (or hidden text), and the icon should be hidden with aria-hidden="true".
  • Match the visible label in the name, so voice control works (WCAG 2.5.3).
  • Keep the name the same when the state changes (see below).
<button type="button" aria-label="Delete sourdough recipe">
<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 24 24">
<path d="M6 7h12l-1 14H7L6 7zm3-4h6l1 2H8l1-2z" />
</svg>
</button>

Toggle Buttons: aria-pressed​

A toggle button switches something on or off, such as "Mute," "Bold," or "Favourite." Use aria-pressed:

<button type="button" aria-pressed="false">Mute</button>

Your script flips the value to "true" and back. The label stays the same, and the state carries the change. If you change the label too ("Mute" to "Unmute") and use aria-pressed, people hear "Unmute, pressed," which is confusing, as the lab showed. Pick one approach:

  • Stable label + aria-pressed (best for toggles), or
  • Changing label with no aria-pressed (fine for simple actions like "Play" and "Pause").

Do not rely on colour alone to show that a toggle is on. Use a border, an icon, or bold text as well, as the lab does.

Disclosure Buttons: aria-expanded​

A button that shows or hides something (a menu, a panel, an accordion section) uses aria-expanded:

<button type="button" aria-expanded="false" aria-controls="details">
Delivery details
</button>
<div id="details" hidden>…</div>

When opened, set aria-expanded="true" and remove hidden from the panel. aria-controls points to the panel (support from screen readers varies, so do not rely on it alone). For simple show-and-hide content, the native details and summary elements do the same job with no script at all.

Disabled Buttons​

The disabled attribute makes a button unavailable. It cannot be clicked, and it is removed from the Tab order. That is sometimes right, but it has a cost: keyboard and screen reader users may never find it, and may not know why it does not work.

Two common choices:

  • Native disabled: simple, and fully blocks the button. Use it when the reason is obvious, and say why in nearby text.
  • aria-disabled="true": the button stays focusable, and is announced as "dimmed" or "unavailable." You must block the action yourself in script, and style it so it looks unavailable. This lets you explain the reason when someone presses it.
index.html
<input type="checkbox" id="agree">
<label for="agree">I accept the terms</label>

<p id="hint">Accept the terms to place your order.</p>

<!-- Option 1: native disabled -->
<button type="button" disabled aria-describedby="hint">Place order</button>

<!-- Option 2: aria-disabled, which stays focusable -->
<button type="button" aria-disabled="true" aria-describedby="hint" id="order">
Place order
</button>

<script>
document.getElementById("order").addEventListener("click", (event) => {
if (event.currentTarget.getAttribute("aria-disabled") === "true") {
// block the action, and explain why
return;
}
placeOrder();
});
</script>

Another option is to leave the button enabled and show an error message when someone presses it too early, which is often the friendliest design of all. Whatever you choose, do not rely on a greyed-out look alone. Disabled text is also exempt from the usual contrast rules, so make sure that the reason is stated in readable text.

Loading States​

When a button starts something that takes time (saving, paying), people need to know it is working, and must not press it twice.

  • Change the text ("Saving...") or show a spinner with a text alternative.
  • Announce it with a live region (see ARIA Live Regions), so screen reader users hear "Saving" and then "Saved."
  • Ignore extra clicks while busy, rather than removing the button.
  • Be careful with disabled. If you disable the button the person just pressed, it loses focus, and keyboard users are thrown back to the top of the page. Use aria-disabled and ignore clicks, or move focus on purpose. See Focus Management.

Make Buttons Easy to See and Hit​

  • Show a clear focus style, and never remove the outline without a better replacement.
  • Contrast: the text needs at least 4.5:1 against the button background, and the button's edge or background needs 3:1 against what is around it (WCAG 1.4.11), so it is recognisable as a button.
  • Size: WCAG 2.2 sets a minimum target size of 24 by 24 CSS pixels (with some exceptions). Around 44 by 44 is a much more comfortable size for touch screens, especially for people with tremors.
  • Space: leave a gap between buttons, so a slip does not hit the wrong one.
  • Look like a button. A border, background, or clear shape helps everyone, and especially people with cognitive differences.
  • Do not use a fixed width. Translated or enlarged text must fit.
button {
min-height: 44px;
padding: 0.5rem 1rem;
border: 2px solid #1d4ed8;
border-radius: 6px;
background: #1d4ed8;
color: #ffffff;
}

button:focus-visible {
outline: 3px solid #0f172a;
outline-offset: 3px;
}

If it goes somewhere, use a link. If it does something, use a button. Do not style one to look like the other without a good reason, because people expect different things, including the key that activates it (Enter only for links, Enter and Space for buttons). The full comparison is in Accessible Links.

Check Your Buttons​

The checker below looks for the problems we have covered: fake buttons made from div and span, links used as buttons, a missing type in a form, missing names, positive tabindex, interactive elements nested in buttons, and disabled buttons that deserve an explanation.

http://127.0.0.1:5500/index.html

On a real site, do these quick tests:

  1. Press Tab. Can you reach every button, and see where you are?
  2. Press Enter and Space on each button. Do both work?
  3. Look in the Accessibility panel. Does each button have a role and a name?
  4. Listen with a screen reader. Do toggle and disclosure buttons announce their state?
  5. Use voice control. Say "click" and the visible label.
  6. Zoom in to 200 percent, and check that the buttons still fit and work.

Good Habits​

  1. Use button, and write its type every time.
  2. Name every button, with a clear verb, and make the visible text part of the name.
  3. Use aria-pressed for on and off, and aria-expanded for show and hide, with a stable label.
  4. Explain why a button is unavailable, in text near it.
  5. Do not disable a button the person is using. Move focus on purpose if you must.
  6. Give feedback when something starts, finishes, or fails.
  7. Make buttons big enough, with clear focus and good contrast.
  8. Ask for confirmation, or offer undo, for actions that cannot be reversed.
  9. Test with a keyboard, a screen reader, and a phone.

Accessibility Checklist​

  • Every button is a real button (or input type="button" or submit).
  • Every button has a type.
  • Every button has a clear accessible name, and icon-only buttons are labelled.
  • Toggle buttons use aria-pressed, and disclosure buttons use aria-expanded, with stable labels.
  • Buttons can be reached with Tab and used with both Enter and Space.
  • Focus is clearly visible.
  • Text and button edges have enough contrast.
  • Targets are large enough, and spaced apart.
  • Disabled and loading states are explained, and announced.
  • Destructive actions have a confirmation, or an undo.

Security and Privacy Notes​

  • Prevent double submits. A second click on "Pay" or "Send" can charge or send twice. Ignore repeat clicks while a request is running, and make the server safe against repeats as well.
  • Confirm or allow undo for destructive actions (delete, pay, share). Make the safe choice, such as Cancel, the default focus in confirmation dialogs.
  • Be honest in labels. A button called "Cancel" that really confirms, or a "Close" that accepts all cookies, is deceptive.
  • Give equal weight to privacy choices. In a consent banner, "Reject" should be as easy to find, reach, and press as "Accept." Hiding it is a dark pattern, and often against privacy law.
  • Do not hide submit buttons or make them invisible. An overlay that tricks people into clicking something else is clickjacking.
  • Do not collect how people press buttons (for example, with a keyboard or a switch) in a way that reveals a disability.
  • Never put secrets in a button's attributes, such as data- values or inline handlers. Anyone can read them.

Global Audience Notes​

  • Translate button text and aria-label values with the rest of the page, and keep buttons wide enough for longer words in other languages. German and Finnish, for example, can be much longer than English.
  • Avoid icon-only buttons when you can. Icons are not universal (a hamburger, a heart, or a hand may mean different things in different places). Add a text label.
  • Right-to-left languages: arrows and "next" or "back" icons should flip to match the reading direction, and button order in a row should follow it too.
  • Voice control users say the visible label in their own language, so the visible text must be in the name.
  • Slow networks and old devices: show a loading state straight away, so people do not press twice and wonder if anything happened.
  • Touch first: many people use only a phone. Large, well-spaced buttons matter more than on a desktop.
  • Verbs travel better than clever wording. "Save," "Send," and "Next" translate well. Puns do not.

Common Mistakes to Avoid​

  1. A div or span used as a button.
  2. A link (a href="#") used as a button.
  3. No type, so a button submits the form by accident.
  4. Icon-only buttons with no name.
  5. Changing the label and using aria-pressed together.
  6. Disabling a button with no explanation.
  7. Disabling the button someone just pressed, which loses focus.
  8. Removing the focus outline.
  9. Tiny buttons close together.
  10. Several buttons that all say "Delete" or "Edit."
  11. A link or another button nested inside a button.
  12. No feedback after pressing.

Practice Time​

Try these in your browser and your editor:

  1. Use the toggle lab with and without the label change, and write down what you hear.
  2. Build a "Favourite" button with aria-pressed, and a "Show more" button with aria-expanded.
  3. Use the disabled lab in both modes. Press Tab from the checkbox, and describe where focus lands.
  4. Replace a div button with a real button, and compare the keyboard behaviour.
  5. Run the checker on the messy page, and fix every problem.
  6. Add type="button" to the buttons in a form you made. Does anything change?
  7. Label an icon-only button, and confirm its name in the Accessibility panel.
  8. Make a "Save" button that shows "Saving..." and announces "Saved" with a live region.
  9. Resize your buttons to 44 pixels tall, and test them on your phone.
  10. Bonus: build a confirmation dialog for "Delete recipe," with focus on the Cancel button.

Quick Recap​

Click a question to check your answer.

1. Why use a real button and not a div?

A button gives you focus, Enter and Space activation, the button role, a name from its content, disabled, and form behaviour for free. A div has none of these.

2. What is the risk of a button with no type inside a form?

It is a submit button by default, so it can submit the form by accident. Write type="button" or type="submit" every time.

3. How do you make a toggle button accessible?

Use aria-pressed, flip it between "true" and "false", and keep the label the same.

4. Which attribute shows that a button opens and closes a panel?

aria-expanded, set to "true" when open and "false" when closed, usually with aria-controls pointing to the panel.

5. What is the difference between disabled and aria-disabled?

disabled blocks the button and removes it from the Tab order. aria-disabled keeps it focusable and announced as unavailable, but you must block the action yourself.

6. Why can disabling a button after it is pressed be a problem?

A disabled button cannot keep focus, so keyboard users lose their place. Use aria-disabled and ignore clicks, or move focus on purpose.

Key Takeaways​

  • Use the real button element. It gives you focus, keyboard support, role, and name for free.
  • Always set the type. Inside a form, the default is "submit."
  • Name every button, with a clear verb, and keep the visible text in the name.
  • Toggle: aria-pressed. Disclosure: aria-expanded. Keep the label stable.
  • Explain disabled and loading states, and announce changes.
  • Make buttons visible, roomy, and high-contrast, with a clear focus style.
  • Protect people from double submits and from destructive mistakes.

Next, learn how to build forms that everyone can complete in Accessible Forms.