# Toggle buttons that don't say whether they are pressed

Source: https://easeweb.dev/learn/toggle-buttons
Topics: Buttons and links > Toggle buttons such as Bold or Favorite say whether they are pressed (WCAG 4.1.2)
The fix: Either add aria-pressed to the button and draw the filled heart from it, or make the toggle a checkbox drawn as the heart, or swap the name between "Add to favorites" and "Remove from favorites". Use only one of these on a control: a name that changes alongside aria-pressed contradicts itself.
Test with: keyboard, screen reader, forced colors, mouse
References: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/button/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-pressed https://inclusive-components.design/toggle-button/

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A product page shows a canvas backpack with a heart button beside its name. Pressing the heart fills it in, and the backpack is added to the shopper's favorites. A screen reader user tabs to the heart and hears "Favorite, button". They press Enter. The heart fills in, and they hear nothing new. When they come back to the button later, it still says "Favorite, button". They can't tell whether the backpack is saved, so they either press again and risk removing it, or go and look for it in their favorites list.

The same failure turns up in any button that stays down once pressed: Bold and Italic in an editor, Mute in a video player, Follow on a profile, and filter chips such as "In stock" or "On sale".

## Why it fails

Success criterion 4.1.2 requires that the state of a user interface component can be determined by software, and that assistive technology is told when it changes. A toggle button has two states, pressed and not pressed. Here the state is only the `is-saved` class that fills the heart. The button has a name and a role, but nothing in the accessibility tree says pressed, so the change is drawn but never announced.

## Who is affected

Favorite and filter toggles often sit in long lists of products or results, so a person checking what they saved has to check each heart in turn. When the state isn't exposed, every one of them is a guess.

## What automation and AI miss

The broken button passes every automated check: it has a role and a name, and nothing marks it as a toggle. No scanner can tell that the class means pressed. Code assistants often get close but contradict themselves. They add `aria-pressed` and also change the name to "Remove from favorites", so it is announced as "Remove from favorites, toggle button, pressed". Or they put `aria-checked` or `aria-selected` on a button, where neither belongs. Press the button and listen to what changes.

## Before: a button that only draws its state

```html
<button type="button" class="fav" aria-label="Favorite">
  <svg aria-hidden="true" viewBox="0 0 24 24"><path d="…"/></svg>
</button>
```

```js
fav.addEventListener('click', () => {
  fav.classList.toggle('is-saved');
});
```

```css
.fav.is-saved svg { fill: currentColor; }
```

The button can be reached with Tab and pressed with Enter and Space, but its state is only the `is-saved` class. A screen reader announces "Favorite, button" whether the backpack is saved or not, and says nothing when it changes.

## After: a toggle button with aria-pressed

```html
<button type="button" class="fav" aria-pressed="false" aria-label="Favorite">
  <svg aria-hidden="true" viewBox="0 0 24 24"><path d="…"/></svg>
</button>
```

```js
fav.addEventListener('click', () => {
  const pressed = fav.getAttribute('aria-pressed') === 'true';
  fav.setAttribute('aria-pressed', String(!pressed));
});
```

```css
.fav[aria-pressed="true"] svg { fill: currentColor; }
```

`aria-pressed` makes the button a toggle button, announced as "Favorite, toggle button, not pressed" and then "pressed" when it changes. The name stays "Favorite" and the state does the rest. The heart is filled from the attribute, not from a class, so the picture and the announcement come from one value and can't drift apart.

## After: a checkbox drawn as the heart

```html
<label class="fav">
  <input type="checkbox" name="favorite" class="visually-hidden">
  <svg aria-hidden="true" viewBox="0 0 24 24"><path d="…"/></svg>
  <span class="visually-hidden">Favorite</span>
</label>
```

```css
.fav:has(:checked) svg { fill: currentColor; }
.fav:has(:focus-visible) { outline: 3px solid; outline-offset: 2px; }
```

The browser holds the state and announces it as "Favorite, checkbox, checked" or "not checked". Space and a click on the heart toggle it with no script, and the form can send it without any. The heart and the focus ring are drawn on the label from the checkbox inside it, because the checkbox itself is hidden.

## After: a name that says what pressing will do

```html
<button type="button" class="fav" aria-label="Add to favorites">
  <svg aria-hidden="true" viewBox="0 0 24 24"><path d="…"/></svg>
</button>
```

```js
fav.addEventListener('click', () => {
  const saved = fav.dataset.saved === 'true';
  fav.dataset.saved = String(!saved);
  fav.setAttribute('aria-label', saved ? 'Add to favorites' : 'Remove from favorites');
});
```

```css
.fav[data-saved="true"] svg { fill: currentColor; }
```

The button stays a plain button, and its name tells people what pressing it will do, which also tells them where it stands: "Remove from favorites" means the backpack is saved. This is the pattern the APG names as the alternative to `aria-pressed`, as in Mute and Unmute. The condition is the announcement. When the name of the focused button changes, some screen readers read the new name and others say nothing, so the person may not learn that the press worked until they move away and back. Test it with the screen readers your users have, or announce the result in a status message as well.

## Which option to use

All three pass 4.1.2, and the right one depends on what the toggle is and what is already there. When you are building a toggle that looks and acts like a button, such as Favorite or Bold, use `aria-pressed`: it keeps one name, is announced on focus and the moment it changes, and costs one attribute. HTML has no toggle button element, so this is not ARIA standing in for a native one. When the toggle is really a choice in a form, such as filters sent together, or the markup is already a checkbox, keep the checkbox: the browser holds the state and it works without script, though it is announced as a checkbox and does not respond to Enter. When the design already changes the label to the next action, as Mute and Unmute do, keep the changing name rather than adding `aria-pressed` on top, and test that the new name is announced, or confirm the result in a status message.

## Implementation decisions

Keep the name the same in both states when you use `aria-pressed` or a checkbox. "Favorite" with a state of pressed is clear. "Remove from favorites, pressed" says two different things at once.

The same choice applies when the label is visible text, as on a Follow button:

- With `aria-pressed`, keep the word "Follow" and draw the pressed look from the attribute.
- With a changing label, make the new word the next action, "Unfollow", not "Following", and leave `aria-pressed` off.
- A label that changes to describe where things stand, such as "Following", tells sighted users the state but reads to everyone else as an action.

Draw the pressed state with more than colour. A heart that changes from outline to filled is a change of shape, which forced colors keeps if the fill uses `currentColor`; a heart that only turns from grey to red disappears there and for people who can't tell the two apart.

Saving a favorite usually means a request to the server. Change the state when the person presses, and if the save fails, set it back and say so in a status message, rather than leaving the heart filled for an item that wasn't saved.

Component libraries often put the pressed state on a wrapper or an icon. Check the element that takes focus, because that is the one whose state is read. When the toggle is a settings row with an on and off position rather than a button, see [toggle switches](/learn/toggle-switches) instead.

## Verify the fix

1. With a screen reader, Tab to the toggle and confirm it announces the name, the role (toggle button, checkbox or button) and the current state.
2. Press it with a click, Space and, for a button, Enter. Confirm each press changes the state exactly once, and the new state or name is announced straight away in each screen reader you support.
3. Confirm the name and the state agree: with `aria-pressed` or a checkbox the name is the same in both states, and with a changing name there is no `aria-pressed`.
4. Reload the page and confirm the drawn state and the announced state both match what was saved.
5. In forced colors, confirm pressed and not pressed can still be told apart.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared button component changes.

## Limits

This is a starting pattern, not a guarantee for every application. Screen readers word toggle buttons differently, and whether a changed name is announced depends on the screen reader and browser. The CSS is a sketch; check the focus indicator and the contrast of the heart against your own palette. Test the complete journey, from saving an item to finding it in the favorites list, with your target assistive technology.
