Testing aria-pressed
aria-pressed indicates the current "pressed" state of a toggle button. It supports three values, true, false, and mixed. It is intended for use on a real <button> element, or an element with role="button".
This page uses a single live example, a set of server controls, to demonstrate all three states, and to demonstrate why mixed exists in the first place: it is a computed consequence of related controls disagreeing, not a state a button starts in or that a user reaches directly.
Example: servers
The "All servers" button controls three individual server buttons. Its own aria-pressed state and label are not set directly, they are recalculated every time a server button changes:
- all three servers off →
aria-pressed="false", labelled "Turn all servers on" - all three servers on →
aria-pressed="true", labelled "Turn all servers off" - a mix of on and off →
aria-pressed="mixed", also labelled "Turn all servers off"
There is no aria-live region on this page. State changes are only announced when focus lands on, or returns to, a button whose aria-pressed or label has changed since it was last focused. This is deliberate: it is the honest, default behaviour of aria-pressed without any extra announcement mechanism layered on top.
Try this:
- Tab to "All servers". It should announce as "Turn all servers on," not pressed.
- Tab to "Turn Server 2 on" and activate it. It should now read "Turn Server 2 off," pressed.
- Shift-tab back to "All servers". It should now read "Turn all servers off," mixed, not "Turn all servers on."
- Turn the remaining two servers on individually. "All servers" should now read "Turn all servers off," pressed, once all three agree.
- Activate "All servers" directly from an off or mixed state. All three servers should turn on and "All servers" should read "Turn all servers off," pressed.
- Activate "All servers" again while it is pressed. All three servers should turn off and "All servers" should return to "Turn all servers on," not pressed.
Expected result: worth confirming directly rather than assuming, whether mixed is announced distinctly from true or false (for example, as "mixed" or "partially pressed" rather than "pressed") has historically varied across screen reader and browser combinations, and is worth checking against your standard test set.
HTML markup
<button id="all-servers" aria-pressed="false">
Turn all servers on
</button>
<button id="server-1" data-name="Server 1" aria-pressed="false">
Turn Server 1 on
</button>
<!-- Server 2 and Server 3 follow the same pattern -->
JavaScript
const allServers = document.getElementById('all-servers');
const serverButtons = Array.from(document.querySelectorAll('.server-button'));
function serverLabel(name, isPressed) {
return isPressed ? `Turn ${name} off` : `Turn ${name} on`;
}
function updateAllServers() {
const states = serverButtons.map(btn => btn.getAttribute('aria-pressed'));
const allOn = states.every(state => state === 'true');
const allOff = states.every(state => state === 'false');
if (allOff) {
allServers.setAttribute('aria-pressed', 'false');
allServers.textContent = 'Turn all servers on';
} else if (allOn) {
allServers.setAttribute('aria-pressed', 'true');
allServers.textContent = 'Turn all servers off';
} else {
// Mixed and all-on share the same label, turning off is always
// the offered action unless every server is already off.
// Colour, not text, is what distinguishes mixed from all-on here.
allServers.setAttribute('aria-pressed', 'mixed');
allServers.textContent = 'Turn all servers off';
}
}
serverButtons.forEach(btn => {
btn.addEventListener('click', () => {
const isPressed = btn.getAttribute('aria-pressed') === 'true';
const nextPressed = !isPressed;
btn.setAttribute('aria-pressed', String(nextPressed));
btn.textContent = serverLabel(btn.dataset.name, nextPressed);
updateAllServers();
});
});
allServers.addEventListener('click', () => {
const currentState = allServers.getAttribute('aria-pressed');
// Only turn everything on if every server is currently off.
// Otherwise (true or mixed), the action is always to turn everything off.
const turnOn = currentState === 'false';
serverButtons.forEach(btn => {
btn.setAttribute('aria-pressed', String(turnOn));
btn.textContent = serverLabel(btn.dataset.name, turnOn);
});
updateAllServers();
});
Design notes
A few deliberate choices in this example are worth calling out rather than leaving implicit:
- Why this feels harder than it should. If you had to click around for a minute before this made sense, that is expected, not a sign you missed something obvious. "All servers" is doing two jobs that pull in opposite directions: it reports the combined state of the three servers upward, but pressing it also commands all three servers downward. Most buttons only do one of those jobs, so a button doing both is an unusual shape to reason about on sight, it only clicks once you have watched it in both directions. There is also a subtler trap in the word "master" or "all": it implies authority or primacy, which nudges people toward assuming the default state should be "on" or "active." Logically, though, a master control that starts unpressed because there is nothing yet to reflect is perfectly consistent, off is simply the state of "nothing has been turned on yet." The mismatch is between the label's connotation and the actual default, not a flaw in the pattern itself.
- Action-based labels. Buttons here say "Turn Server 1 off," not a status like "Server 1: off." This is a common real-world pattern, but it is worth knowing it is in tension with
aria-presseditself: a changing action label already tells the user what will happen, so pairing it witharia-pressedmeans the state is communicated twice, once in the visible text and once in the attribute. It is used here because it is realistic and because the underlyingaria-pressedvalue is still needed to compute the "all servers" mixed state, not because it is the cleanest possible pattern. - Colour choices. Green and grey were chosen because they carry existing conventional meaning (a live indicator light, a disabled switch), unlike an arbitrary colour that has to be learned from scratch. Red was deliberately avoided for the "on" state, since red typically signals danger or error, not "currently active," and would have sent the wrong message here.
- Resolve direction. Pressing "All servers" always turns everything off, unless every server is already off, in which case it turns everything on. The reasoning: turning off is treated as the safer default action any time there is something to turn off. This is a deliberate design decision, not something the spec dictates, and it is worth noting it is the opposite resolve direction from a "mute all" style control, where pressing while mixed typically mutes everything rather than unmuting it. Neither direction is more correct in the abstract, it depends on which action makes sense as the safe default for the scenario.
- Why mixed still needs its own colour. Because "mixed" and "all on" both offer the same action ("Turn all servers off"), the button text cannot distinguish them. Colour is doing real, necessary work here, not just decoration, so relying on colour alone for that specific distinction is a deliberate trade-off worth flagging, not an oversight. A production implementation may want a non-colour cue for this too.
Note for testing: confirm the mixed state announcement, and that "mixed" and "all on" are correctly distinguished by colour when they share the same button text, with your standard browser and screen reader combinations before treating them as settled.