Dynamic accessible descriptions reference
I recently needed to test the support of a dynamic accessible description – a element’s description that is initially one (or no) value, then changes to another value after an action takes place.
I created a page of examples to test and a document to record my findings, but then realized that I could just make this a blog post that I periodically update. That way, others can use it as a reference as well, if they choose.
Testing scope #
I tested four common desktop and two mobile browser and screen reader combinations. All screen readers were using their default settings.
Desktop #
- JAWS (2026.2602.49) + Chrome (146) on Windows 11
- NVDA (2025.3.3) + Firefox (148) on Windows 11
- Narrator (Windows 11 v25H2) + Edge (146) on Windows 11
- VoiceOver + Safari on MacOS 26.3.1
Mobile #
- TalkBack (13.5) + Chrome (146) on Android 11, Samsung Galaxy A20s
- VoiceOver + Safari on iOS 26.3.1, iPhone 17 Pro
Test elements #
I tested changing the descriptions of two elements: a button and a text input. This isn’t inclusive of every element you could add an accessible description to, but they’re both very common use cases.
html snippet: Test elements markup
html<button>Apple</button> <input type="text" aria-label="Apple" value="Fuji">
button element #
The button contains the text “apple”. Activating the button triggers its description to change.
Text input #
The text input has an accessible name of “apple” (via the aria-label property) and contains the text “fuji”.
If the enter key is pressed while focused, the input's accessible description is changed.
Testing methods #
Desktop testing #
Desktop elements were tested in five ways:
- When navigated to with the virtual cursor before the change
- When navigated to with the virtual cursor after the change
- When navigated to with the tab key before the change
- When navigated to with the tab key after the change
- The announcement made while focused when the change occurs
The virtual cursor and the tab key are not the only ways that screen reader users navigate, but they give a reasonable approximation of support that can be supplemented with further testing.
Mobile testing #
The mobile screen readers were tested in five ways:
- When navigated to with the virtual cursor via swipes before the change
- When navigated to with the virtual cursor via swipes after the change
- When navigated to via "controls" before the change
- When navigated to via "controls" after the change
- The announcement made while focused when the change occurs
Regerding navigating via "controls": on TalkBack this means using the "Controls" reading control; while on iOS VoiceOver this means using the "Form Controls" rotor option.
Furthermore, there’s no easy way to trigger the enter key event of the input with the mobile devices’ virtual keyboards, so the change announcement column won’t have results in the text input for those screen readers.
Results #
Here are the approaches I tested and how they performed.
Approach #1: adding a new aria-describedby attribute with value #
For this approach, the elements initially have no accessible description.
After being activated, the aria-describedby attribute is added with a value matching the id of a third element. This should give the elements an accessible description of “fruit”.
html snippet: Approach #1 markup
html<!-- Approach #1: Adding new 'aria-describedby' attribute with value --> <!-- before --> <button>Apple</button> <input type="text" value="Fuji" aria-label="Apple"> <!-- after --> <button aria-describedby="desc1">Apple</button> <input type="text" value="Fuji" aria-label="Apple" aria-describedby="desc1"> <p id="desc1">Fruit</p>
button element #
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via tab |
button after change, via tab |
button on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple button" | "apple button fruit" | "apple button" | "apple button fruit" | "fruit" |
| NVDA + Firefox | "button apple" | "button apple" | "apple button" | "apple button fruit" | "fruit" |
| Narrator + Edge | "apple button" | "apple button fruit" | "apple button" | "apple button fruit" | [no announcement] |
| MacOS VoiceOver + Safari | "apple button" | “apple fruit button” | "apple button" | “apple fruit button” | [no announcement] |
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via "controls" |
button after change, via "controls" |
button on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "apple button" | "apple button fruit" | "apple button" | "apple button fruit" | [no announcement] |
| iOS VoiceOver + Safari | "apple button" | "apple button" | "apple button" | "apple description fruit button" | "apple" |
Text input #
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via tab |
input after change, via tab |
input on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple edit fuji" | "apple edit fuji" | "apple edit fuji type text" | "apple edit fuji fruit type text" | "fruit" |
| NVDA + Firefox | "apple edit fuji" | "apple edit fuji" | "apple edit selected fuji" | "apple edit fruit selected fuji" | "fruit" |
| Narrator + Edge | "apple edit fuji" | "apple edit fuji fruit" | "apple edit fuji" | "apple edit fuji fruit" | [no announcement] |
| MacOS VoiceOver + Safari | "fuji contents selected apple edit text" | “fuji contents selected apple fruit edit text” | "fuji contents selected apple edit text" | “fuji contents selected apple fruit edit text” | [no announcement] |
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via "controls" |
input after change, via "controls" |
input on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "fuji edit box apple" | "fuji edit box apple fruit" | "fuji edit box apple" | "fuji edit box apple fruit" | N/A |
| iOS VoiceOver + Safari | "apple fuji text field" | "apple description fruit fuji text field" | "apple fuji text field" | "apple description fruit fuji text field" | N/A |
Approach #2: changing the text content of element already linked via the aria-describedby attribute #
This approach gives the elements an aria-describedby attribute from the start, which sets an accessible description of “fruit”.
After being activated, the text content of desc2 changes, making the new description “vegetable”.
html snippet: Approach #2 markup
html<!-- Approach #2: Changing the text content of element --> <!-- already linked via the 'aria-describedby' attribute --> <button aria-describedby="desc2">Apple</button> <input type="text" value="Fuji" aria-label="Apple" aria-describedby="desc2"> <!-- before --> <p id="desc2">Fruit</p> <!-- after --> <p id="desc2">Vegetable</p>
button element #
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via tab |
button after change, via tab |
button on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple button fruit" | "apple button vegetable" | "apple button fruit" | "apple button vegetable" | "vegetable" |
| NVDA + Firefox | "button apple" | "button apple" | "apple button fruit" | "apple button vegetable" | "vegetable" |
| Narrator + Edge | "apple button fruit" | "apple button vegetable" | "apple button fruit" | "apple button vegetable" | [no announcement] |
| MacOS VoiceOver + Safari | “apple fruit button” | “apple vegetable button” | “apple fruit button” | “apple vegetable button” | [no announcement] |
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via "controls" |
button after change, via "controls" |
button on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "apple button fruit" | "apple button vegetable" | "apple button fruit" | "apple button vegetable" | [no announcement] |
| iOS VoiceOver + Safari | "apple description fruit button" | "apple description fruit button" | "apple description fruit button" | "apple description vegetable button" | "apple description fruit" |
Text input #
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via tab |
input after change, via tab |
input on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple edit fuji" | "apple edit fuji" | "apple edit fuji fruit type text" | "apple edit fuji vegetable type text" | "vegetable" |
| NVDA + Firefox | "apple edit fuji" | "apple edit fuji" | "apple edit fruit selected fuji" | "apple edit vegetable selected fuji" | [no announcement] |
| Narrator + Edge | "apple edit fuji fruit" | "apple edit fuji vegetable" | "apple edit fuji fruit" | "apple edit fuji vegetable" | [no announcement] |
| MacOS VoiceOver + Safari | “fuji contents selected apple fruit edit text” | “fuji contents selected apple fruit edit text” | “fuji contents selected apple fruit edit text” | “fuji contents selected apple edit fruit text” | [no announcement] |
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via "controls" |
input after change, via "controls" |
input on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "fuji edit box apple fruit" | "fuji edit box apple vegetable" | "fuji edit box apple fruit" | "fuji edit box apple vegetable" | N/A |
| iOS VoiceOver + Safari | "apple description fruit fuji text field" | "apple description vegetable fuji text field" | "apple description fruit fuji text field" | "apple description vegetable fuji text field" | N/A |
Approach #3: adding a new aria-description attribute with value #
Here, the elements begin with no accessible description.
Upon activation, the aria-description attribute with a value of “fruit” is added, making that the new accessible description.
html snippet: Approach #3 markup
html<!-- Approach #3: Adding new 'aria-description' attribute with value --> <!-- before --> <button>Apple</button> <input type="text" value="Fuji" aria-label="Apple"> <!-- after --> <button aria-description="fruit">Apple</button> <input type="text" value="Fuji" aria-label="Apple" aria-description="fruit">
button element #
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via tab |
button after change, via tab |
button on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple button" | "apple button fruit" | "apple button" | "apple button fruit" | "fruit" |
| NVDA + Firefox | "button apple" | "button fruit apple" | "apple button" | "apple button fruit" | "fruit" |
| Narrator + Edge | "apple button" | "apple button fruit" | "apple button" | "apple button fruit" | [no announcement] |
| MacOS VoiceOver + Safari | "apple button" | "apple fruit button" | "apple button" | "apple fruit button" | [no announcement] |
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via "controls" |
button after change, via "controls" |
button on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "apple button" | "apple button fruit" | "apple button" | "apple button fruit" | [no announcement] |
| iOS VoiceOver + Safari | "apple button" | "apple button" | "apple button" | "apple description fruit button" | "apple" |
Text input #
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via tab |
input after change, via tab |
input on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple edit fuji" | "apple edit fuji fruit" | "apple edit fuji type text" | "apple edit fuji fruit type text" | "fruit" |
| NVDA + Firefox | "apple edit fuji" | "apple edit fruit fuji" | "apple edit selected fuji" | "apple edit fruit selected fuji" | "fruit" |
| Narrator + Edge | "apple edit fuji" | "apple edit fuji fruit" | "apple edit fuji" | "apple edit fuji fruit" | [no announcement] |
| MacOS VoiceOver + Safari | "fuji contents selected apple edit text" | "fuji contents selected apple fruit edit text" | “fuji contents selected apple edit text” | “fuji contents selected apple fruit edit text” | [no announcement] |
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via "controls" |
input after change, via "controls" |
input on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "fuji edit box apple" | "fuji edit box apple fruit" | "fuji edit box apple" | "fuji edit box apple fruit" | N/A |
| iOS VoiceOver + Safari | "apple fuji text field" | "apple description fruit fuji text field" | "apple fuji text field" | "apple description fruit fuji text field" | N/A |
Approach #4: changing value of existing aria-description attribute #
This last approach sees the elements begin with the aria-description value of “fruit”.
After being activated, that value changes to “vegetable”, making that the new accessible description.
html snippet: Approach #4 markup
html<!-- Approach #4: Changing value of existing 'aria-description' attribute --> <!-- before --> <button aria-description="fruit">Apple</button> <input type="text" value="Fuji" aria-label="Apple" aria-description="fruit"> <!-- after --> <button aria-description="vegetable">Apple</button> <input type="text" value="Fuji" aria-label="Apple" aria-description="vegetable">
button element #
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via tab |
button after change, via tab |
button on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple button fruit" | "apple button vegetable" | "apple button fruit" | "apple button vegetable" | "vegetable" |
| NVDA + Firefox | "button fruit apple" | "button vegetable apple" | "apple button fruit" | "apple button vegetable" | "vegetable" |
| Narrator + Edge | "apple button fruit" | "apple button vegetable" | "apple button fruit" | "apple button vegetable" | [no announcement] |
| MacOS VoiceOver + Safari | “apple fruit button” | “apple vegetable button” | “apple fruit button” | “apple vegetable button” | [no announcement] |
| Browser / screen reader | button before change, via virtual cursor |
button after change, via virtual cursor |
button before change, via "controls" |
button after change, via "controls" |
button on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "apple button fruit" | "apple button vegetable" | "apple button fruit" | "apple button vegetable" | [no announcement] |
| iOS VoiceOver + Safari | "apple description fruit button" | "apple description fruit button" | "apple description fruit button" | "apple description vegetable button" | "apple description fruit" |
Text input #
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via tab |
input after change, via tab |
input on change |
|---|---|---|---|---|---|
| JAWS + Chrome | "apple edit fuji fruit" | "apple edit fuji vegetable" | "apple edit fuji fruit type text" | "apple edit fuji vegetable type text" | "vegetable" |
| NVDA + Firefox | "apple edit fruit fuji" | "apple edit vegetable fuji" | "apple edit fruit selected fuji" | "apple edit vegetable selected fuji" | "vegetable" |
| Narrator + Edge | "apple edit fuji fruit" | "apple edit fuji vegetable" | "apple edit fuji fruit" | "apple edit fuji vegetable" | [no announcement] |
| MacOS VoiceOver + Safari | “fuji contents selected apple fruit edit text” | “fuji contents selected apple vegetable edit text” | “fuji contents selected apple fruit edit text” | “fuji contents selected apple vegetable edit text” | [no announcement] |
| Browser / screen reader | input before change, via virtual cursor |
input after change, via virtual cursor |
input before change, via "controls" |
input after change, via "controls" |
input on change |
|---|---|---|---|---|---|
| TalkBack + Android Chrome | "fuji edit box apple fruit" | "fuji edit box apple vegetable" | "fuji edit box apple fruit" | "fuji edit box apple vegetable" | [no announcement] |
| iOS VoiceOver + Safari | "apple description fruit fuji text field" | "apple description vegetable fuji text field" | "apple description fruit fuji text field" | "apple description vegetable fuji text field" | N/A |
Key takeaways #
From looking at the results tables for the approaches I tested, there was no one method that worked across the board for relaying a dynamic accessible description. With that in mind, here’s a few things that I noticed.
Narrator, MacOS VoiceOver, and TalkBack are solid; JAWS and NVDA are ok; and iOS VoiceOver is bad #
Those first three screen readers (with the browsers they were paired with here) did a good job, correctly announcing accessible descriptions across all variants and methods.
Meanwhile, JAWS and NVDA did well in some places and poor in others. iOS VoiceOver can’t be trusted at all.
Navigation methods are not equal #
Of the few holes of support remaining on these tables, they tend to be when navigating with the virtual cursor. While navigating via the tab key saw the accessible description being read correctly in all desktop screen readers.
JAWS and NVDA’s virtual cursor struggled with reading descriptions derived from the aria-describedby attribute, but not aria-description. Not what I would expect from two of the biggest names in the screen reader space.
Accessible description changes aren’t announced live #
Some of the screen readers – JAWS, NVDA, and iOS VoiceOver – made an announcement the moment an element’s description changed. Some of those announcements were oddly correct even when navigating to the element moments earlier didn't get the correct announcement.
Ironically, Narrator, MacOS VoiceOver, and TalkBack – the three screen readers here that performed the best at announcing descriptions of elements they encounter – did not announce anything when a item’s description changed. Depending on if you think accessible descriptions should act like live regions or not, this may or may not be an issue for you.
If it’s imperative that information in an accessible description is announced as it changes, you can’t rely on that happening using any of the products.
aria-description has better support than aria-describedby #
This one was a shock to me. aria-describedby has been around for what seems like forever as part of the ARIA (Accessible Rich Internet Applications) 1.0 specification. Whereas the much-newer aria-description is part of ARIA 1.3 that’s not even standardized yet.
aria-describedby had decent support, but some glaring holes in JAWS, NVDA, and iOS VoiceOver.
aria-description, on the other hand, had full support across everything I tested EXCEPT for iOS VoiceOver, where it worked when navigating via the rotor but not with the virtual cursor.
iOS VoiceOver STILL has big issues with accessible descriptions #
In previous versions of this article, testing showed that both MacOS and iOS VoiceOver had big issues with accessible descriptions. But since my last test, MacOS VoiceOver has improved a ton – supporting everything except announcing live changes to a description.
This leaves iOS VoiceOver as the biggest loser. iOS VoiceOver seems to now be able to recognize an initial accessible description, but no change is picked up when swiping around with the virtual cursor.
But interestingly enough, navigating via the rotor’s “form controls” DOES announce dynamic changes to an element’s description. And to take it one step further, switching from the “form controls” rotor back to using swipes to navigate will announce the correct description.
This tells me that iOS VoiceOver uses some sort of cache or buffer of its accessibility tree that refreshes when jumping around with the rotor but not when using standard swipes.
If and when iOS VoiceOver improves its support of accessible descriptions, it will finally make some of these workflows viable.
Test page #
If you’d like to visit the test page yourself, I hosted it on CodePen:
Conclusion #
As mentioned, I will try to periodically update this page as browsers and screen readers receive updates. Hopefully, the support gaps here will be fixed at some point, giving accessible descriptions wide and reliable support.
Questions or comments? Feel free to email me or ping me on Mastodon.
Update – 06/11/2024 #
Yesterday, Apple announced the upcoming Safari 18 and made a beta version available to developers. In the Safari 18 beta release notes, under the "Bug Fixes and More" and "Accessibility" headings is this bit:
Fixed updating aria-describedby text after the targeted element changes its subtree.
I interpret this to say that changes to an element's content that provides the accessible description for another element via aria-describedby will now correctly update the element's description. However, after downloading the iOS 18 beta and testing this out, I did not find this to be true.
I'll continue to monitor this out as more Safari 18 betas are released. I'm hoping that Apple/WebKit may finally improve it's accessible description support for Safari + VoiceOver users.
Update – 10/31/2024 #
It's been almost a year, so I decided to update all of my screen readers and browsers to do a retest of this whole article. While there's still a good amount of support holes, JAWS has made some minor improvements (while regressing in a couple of spots), and MacOS VoiceOver has improved a lot.
Also noteworthy for MacOS VoiceOver is that it ditched its cumbersome workflow for accessible descriptions, which required users to listen to a "more content available" announcement when an item had a description. Users would then have to use the shortcut CTRL + OPTION + COMMAND + / (slash) to open up a context menu where they could access the description. I'd glad that this has been removed.
The test results tables and key takeaways have been updated to reflect how things stand as of this date.
Update – 03/11/2026 #
Since it had been almost a year and a half, I decided to retest and update this article once again. JAWS and NVDA saw a couple of minor improvements, while MacOS VoiceOver saw a large one. iOS Voiceover continues to be subpar.
The results tables and takeaways have been updated to reflect these most recent tests.