Toggle button pattern with aria-pressed and reliable status announcements for screen readers
The wishlist button is one of the most unassuming yet most frequently mismarked interaction elements in an online store: a heart icon flips its state on click without any visible change to the surrounding markup, and that silent status change is invisible to screen reader users without extra ARIA work. Applying the toggle button pattern with aria-pressed consistently, and actively announcing status changes, turns a purely visual switch into a traceable action, including while managing the wishlist itself.
Table of Contents
- 1. Why the wishlist button is its own status change problem
- 2. The toggle button pattern: aria-pressed instead of aria-checked
- 3. Implementation: a heart icon button with switching label and state
- 4. Actively announcing status changes for screen readers
- 5. Icon only buttons: why aria-label and contrast matter for the wishlist icon
- 6. Wishlist overview: focus management when removing items
- 7. Moving to cart: announcing two events within one action
- 8. Building multiple wishlists and shared lists accessibly
- 9. Implementation in Magento and Hyvä: an Alpine.js component for the wishlist button
- 10. Summary
- 11. FAQ
1. Why the wishlist button is its own status change problem
Unlike plain form validation or a classic add to cart button, a wishlist button triggers no navigation and no visible new form field, it simply flips a boolean state: saved or not saved. That very simplicity makes it tricky, because development teams often treat it like an ordinary button with no special state, even though it is technically distinct from a plain action button.
A wishlist button is not a link, not a form submit, and not a simple action button, it is what is called a toggle button: a button that switches between exactly two states and keeps displaying that state until clicked again. The ARIA specification defines a dedicated attribute for this interaction type, one that is surprisingly often missing or misused in practice.
2. The toggle button pattern: aria-pressed instead of aria-checked
For a wishlist button, aria-pressed is the correct attribute, not aria-checked. aria-checked belongs to radio buttons, checkboxes and menu items with checkbox behavior and implies group membership or a form like selection. aria-pressed, on the other hand, is built exactly for the case where a single, standalone button switches between a pressed and an unpressed state, exactly the behavior of a wishlist heart.
It matters that aria-pressed is set on the button element itself and toggled between "true" and "false" via JavaScript on every click. If the attribute is missing entirely, the screen reader reports only a button with no state information at all, and the user cannot tell whether an item is already on the wishlist without checking the page visually.
<button
type="button"
class="wishlist-toggle"
aria-pressed="false"
data-product-id="1234"
onclick="toggleWishlist(this)">
<svg class="wishlist-toggle__icon" aria-hidden="true"><!-- heart icon --></svg>
<span class="wishlist-toggle__label">Add to wishlist</span>
</button>
3. Implementation: a heart icon button with switching label and state
A purely visual icon that changes from an outlined to a filled heart communicates the new state only to sighted users. To keep the same switch textually understandable, the button's label should be updated alongside aria-pressed: after the click, "Add to wishlist" becomes "Remove from wishlist", not just the visual state of the icon.
This combination of aria-pressed and a switching text is doubly redundant, and precisely because of that it is robust: some screen reader modes prefer to read out the label, others read the state information from aria-pressed. If only one of the two is maintained, the announcement works incompletely in certain screen reader configurations.
function toggleWishlist(button) {
const pressed = button.getAttribute('aria-pressed') === 'true';
const nextState = !pressed;
button.setAttribute('aria-pressed', String(nextState));
button.querySelector('.wishlist-toggle__label').textContent = nextState
? 'Remove from wishlist'
: 'Add to wishlist';
syncWishlist(button.dataset.productId, nextState);
}
4. Actively announcing status changes for screen readers
In many screen reader combinations, a change to aria-pressed gets read out the next time the button is focused, but not automatically at the moment the click happens, if focus stays on the button. For a reliable, immediate response, an additional discreet aria-live="polite" region is recommended, briefly confirming that the item was added to or removed from the wishlist.
This live region should stay visually unobtrusive, for example as an sr-only element, and replace its text on every status change rather than appending to it, so screen readers do not repeatedly read out old messages again. A double announcement, once through aria-pressed and once through the live region, is not a contradiction here, it increases reliability across different assistive technologies.
<div id="wishlist-status" class="sr-only" aria-live="polite"></div>
<script>
function syncWishlist(productId, added) {
document.getElementById('wishlist-status').textContent = added
? 'Item was added to the wishlist.'
: 'Item was removed from the wishlist.';
}
</script>
5. Icon only buttons: why aria-label and contrast matter for the wishlist icon
In product listings, the wishlist button is often shown as an icon only, without visible text, for space reasons. In that case, a switching aria-pressed alone is not enough, because a button without visible text also has no accessible name. An aria-label that also updates per state then takes over the role of the label from the previous example.
Visually, a filled heart often differs from an outlined one only by a thin contour line, which is hard to recognize with low contrast or visual impairments. An additional, even subtle visual signal such as a color change or a small badge improves recognizability for users who can see but struggle with fine shape differences.
<button
type="button"
class="wishlist-icon-button"
aria-pressed="true"
aria-label="Remove from wishlist"
data-product-id="1234">
<svg class="wishlist-icon-button__icon" aria-hidden="true"><!-- filled heart --></svg>
</button>
6. Wishlist overview: focus management when removing items
On the actual wishlist page, every click on the remove button takes a complete list item out of the DOM. Without deliberate focus management, the currently focused element disappears along with it, and keyboard focus falls back uncontrolled to the top of the document or the body, causing users to lose their place in the list.
It is more robust to move focus deliberately to the remove button of the next list item after removal, or, if no items remain, to a heading or an empty state message like "Your wishlist is empty". In addition, an aria-live="polite" region should confirm which item was removed, so the action remains traceable without visual observation.
7. Moving to cart: announcing two events within one action
The "move to cart" action technically triggers two events at once: the item disappears from the wishlist and appears in the cart. If only one of the two events is announced, for example only a generic cart confirmation, it stays unclear to screen reader users that the item was simultaneously removed from the wishlist.
A clear, combined announcement like "Item was moved to the cart and removed from the wishlist" in the same live region used for a plain removal action avoids this misunderstanding and prevents users from later mistakenly looking for the same item, assumed still present, on the wishlist.
function moveToCart(productId) {
removeFromWishlist(productId);
addToCart(productId);
document.getElementById('wishlist-status').textContent =
'Item was moved to the cart and removed from the wishlist.';
}
8. Building multiple wishlists and shared lists accessibly
Stores with multiple named wishlists, for example for different occasions, or with a share by link feature need additional status announcements. Creating a new list should move focus directly to the new name field, while copying a share link should be confirmed through the same aria-live technique already used for the wishlist toggle, for instance with the text "Link was copied to the clipboard".
With multiple lists, the wishlist button on the product view should clearly convey which list an item is currently saved to, for example via a dropdown with checkbox semantics instead of a simple toggle button, since this is no longer a plain yes or no decision but a multi select across several lists.
9. Implementation in Magento and Hyvä: an Alpine.js component for the wishlist button
In Hyvä, the wishlist toggle can be implemented as a compact Alpine.js component that holds the saved state, updates the label in sync with aria-pressed, and uses a central live region in the layout for the status announcement. The component should be reused in the product listing, the product detail page and the wishlist overview, so behavior does not differ from page to page.
Every inline script block for the status announcement or for toggling aria-pressed must be registered via $hyvaCsp->registerInlineScript() in the corresponding phtml template, so the Content Security Policy does not block execution.
document.addEventListener('alpine:init', () => {
Alpine.data('wishlistToggle', (productId, initialPressed) => ({
pressed: initialPressed,
toggle() {
this.pressed = !this.pressed;
this.$dispatch('wishlist-status', {
message: this.pressed
? 'Item was added to the wishlist.'
: 'Item was removed from the wishlist.',
});
},
}));
});
| Element | Attribute/technique | Purpose | Common mistake |
|---|---|---|---|
| Wishlist button | aria-pressed true/false | Announcing state for screen readers | Attribute missing entirely |
| Icon only button | aria-label per state | Providing an accessible name without visible text | Static label despite state change |
| Status change | aria-live=polite region | Immediate confirmation of the action | Only a visual change with no announcement |
| Removing an item | Focus on next element | Preserving orientation within the list | Focus falls back to the top of the document |
| Moving to cart | Combined live announcement | Communicating both events at once | Only a cart confirmation with no wishlist mention |
Mironsoft
WCAG audits, accessible Magento shops, and training
Not sure whether the shop is actually accessible?
We audit existing Magento shops against WCAG 2.2, fix concrete barriers in the Hyvä frontend, and train teams so accessibility stays anchored in the development process for good.
WCAG Audit
Systematically review the shop against WCAG 2.2 AA, with a prioritized issue list.
Fixing Barriers
Concrete implementation: keyboard operability, screen reader support, contrast, forms.
Team Training
Raise developer and editor awareness for accessible implementation day to day.
10. Summary
Accessible Wishlists: The Essentials
Core idea
aria-pressed instead of aria-checked correctly marks the wishlist button as a toggle button with two states.
Double redundancy
A switching label or aria-label combined with aria-pressed and a live region secures the announcement across different screen readers.
Focus management
Removing an item from the wishlist list requires moving focus deliberately to the next element or an empty state message.
Combined actions
Moving to cart affects two lists at once and needs a correspondingly combined status message.