Playing with state
In an effort to convince everyone (who reads these blog posts) that I don't only obsess over tooltips, let's talk about another seemingly simple concept and make it unexpectedly complex. This time, we're tackling the play/pause toggle button.
By a play/pause toggle, I mean this thing:
i.e. that thing you click on (or key press/touch/switch/etc) to get your daily cute cat fix. You press it, it switches from a play icon to a pause icon, Maru jumps into a box, and bliss ensues.
That's where I barge in and ask about that middle part -- switching from play to pause -- and ruin it all (sorry Maru). The thing is, a play/pause button is effectively a toggle button, by which I mean it switches between two binary states based on user interaction. The established pattern for accomplishing this is by updating the
aria-pressed state attribute, which accepts a true/false value.
Play/pause buttons (and by extension, start/stop buttons) are the black sheep of the toggle button family. They generally do not have any
aria-pressed on/off state defined, and instead change their accessible name from "play" to "pause" when activated. When I say "generally," I mean this was true for the following sites, chosen for no formal reason other than that I happened to know they would contain media players:
- Youtube: dynamically changes
- Mixer: dynamically changes
- Vimeo: dynamically changes name from contents (by swapping out labelled graphics)
- Soundcloud: dynamically changes both text content and the
- WAI carousel tutorial: dynamically changes text content
Twitter's media player: Twitter's media player actually has no accessible name and no state defined for its play button. Whoops.
So why does this matter? Traditional toggle buttons switch
aria-pressed from true to false, and play/pause buttons change their calculated name from "play" to "pause." No big deal?
Property vs. State
Most dynamic changes to a UI component (at least, changes that happen while a user is interacting with it) are communicated through state changes rather than property changes. The ARIA spec has this to say about states vs. properties:
One major difference is that the values of properties (such as aria-labelledby) are often less likely to change throughout the application life-cycle than the values of states (such as aria-checked) which may change frequently due to user interaction. Note that the frequency of change difference is not a rule; a few properties, such as aria-activedescendant, aria-valuenow, and aria-valuetext are expected to change often.
It might therefore seem reasonable to also expect a screen reader to pick up and announce state changes but not property changes. However, as with many things related to ARIA, it is not that simple.
Some properties such as
aria-valuetext can be expected to be announced by screen readers when changed (support issues aside). Some state changes (such as
aria-disabled) are not consistently announced when changed. However, as a very general rule of thumb, it is safer to assume that a change in state will be communicated than a change in property (and please never change a role during a user interaction).
Side note: when I reference changes that are announced by screen readers, I mean a change to the element that currently has focus that causes some sort of screen reader-generated feedback without the user moving focus.
Most screen readers nowadays rely on Accessibility API events to get notifications about changes to the DOM. So, for example, when
aria-pressed updates from
PropertyChangedEvent will fire, allowing a screen reader to listen to that event and react to it. Each platform's Accessibility API handles this slightly differently (
PropertyChangedEvent is specific to UIA on Windows), but the principal is roughly the same. This list of state and property change events details which states and properties should raise API events when changed. Not all of those states and properties will necessarily be communicated by all screen readers, but these are the ones that at least have a mechanism to do so.
All this is relevant since
aria-pressed is a state, and the accessible name is a property.
The conventional wisdom is to not change the name of a control while the user is interacting with it. It turns out this is pretty good conventional wisdom: upon testing, I found that while a name change is sometimes announced, it is not nearly consistent enough to be relied upon.
Using this button code sample, I tested whether name changes and
aria-pressed changes were announced using the following screen reader/browser combinations, and using a few different methods of defining the accessible name:
|NVDA + Firefox||yes||no||no||yes|
|NVDA + Chrome||yes||yes||no||yes|
|NVDA + Edge||yes||yes||yes||yes|
|JAWS + Firefox||no||no||no||yes|
|JAWS + Chrome||no||no||no||yes|
|JAWS + Edge||no||no||no||yes|
|JAWS + IE 11||yes||yes||yes||yes|
|Narrator + Edge||yes||yes||yes||yes|
|iOS VoiceOver + Safari||yes||yes||yes||yes|
|macOS VoiceOver + Safari||yes||yes||yes||yes|
|Talkback + Chrome||no||no||no||yes|
Side note: NVDA, you drunk?
You made it this far, congratulations! Time for some concrete recommendations. The big takeaway should be this:
Change the name, but not the state, of play/pause buttons. Use state for all other toggle buttons.
Why? First, because the mechanics of a play/pause (or start/stop) button are so well understood by now that immediate state change feedback is not critical.
This is combined with the fact that using
aria-pressed="false" for a pause button would result in some variation of "play button off" to be read by screen readers, which is not particularly reflective of a visual pause icon. Creating a difference between the programmatic label and visual text (or icon) can cause issues for speech control users, sighted screen reader users, and effective communication between blind screen reader users and visual users (e.g. during a support call).
This one edge case does not take away the general recommendation to change the
aria-pressed state, and not the name, for other toggle buttons. Toggle buttons that are part of less well-known interfaces would benefit more from providing immediate feedback, and the only reliable cross-screen-reader cross-browser way to do that is to use
As a final note, never change both the name and
aria-pressed state in tandem. That way, confusion lies. Just imagine trying to parse the meaning of "play button, on" vs. "pause button, off".