Screen readers and drag-and-drop, part 2: grabbing and releasing elements
I know it’s been awhile (*checks notes* wow, almost two years!), but I’m finally back with the next article in my “screen readers and drag-and-drop” article series. Here, I’ll be diving into how to convey to screen reader users when an element has been picked up or put down. Ideally, I’ll be able to identify at least a few approaches to prototype into full patterns to test later.
Most screen reader users have limited to no vision and will miss visual cues showing an item’s been grabbed. So it’s important to communicate these actions programmatically as well so that everyone can perceive the interaction.
Building off of part 1 #
If you missed it or need a refresher, the previous article covered how to communicate an element’s draggability to screen readers. You can find part 1: draggable elements here.
Lets summarize the three promising approaches from part 1. The goal was to communicate to screen readers that an item can be dragged.
- Draggable item #1 – Static text: this method places the drag information in the item’s content, making it part of the item’s canonical name. Example: “apple draggable, button”.
- Draggable item #2 –
aria-describedbyattribute: this property points to another element containing the drag information, making it part of the item’s accessible description that is often announced following its name and role. Example: “apple, button, draggable”. - Draggable item #3 –
aria-roledescriptionattribute: this property changes the item’s announced role to a custom value indicating draggability. Example: “apple, draggable button” instead of “apple, button”.
Each has its pros and cons, but they all do the job of indicating that their item is draggable. The next step is to communicate changes to an item being picked up and put down.
Terminology #
I used the terms “drag”, “draggable”, and “draggability” quite a bit in part 1. So using the word “dragging” here could add confusion.
To make things easier, I’ll refer to the act of picking up and putting down items as “grabbing” and “releasing” whenever possible. The way I see it, an item may be draggable, but first must be grabbed before doing so.
Lastly, the information associated with an element’s current status will be referred to as its “grab state”.
Semantics and state #
Ideally, the native Drag-and-Drop API (DNDAPI) would provide functionality and information for keyboard and screen reader users right out of the box, but that’s not the case.
For this article’s scope, it could hypothetically use an HTML attribute that conveys the semantics that 1) an item can be dragged; and, 2) whether it’s in a grabbed state or not. The DNDAPI has the draggable attribute, but that doesn’t pass along any kind of semantic information.
The ARIA (Accessible Rich Internet Applications) specification’s aria-grabbed property does actually do this in the few places it’s supported (JAWS and NVDA) — an aria-grabbed value of false results in a “draggable” state being announced, while a true value gives a state announcement of “dragging” (NVDA) or “grabbed” (JAWS). But the aria-grabbed property is both deprecated and has poor support, so it’s not an option either.
Because there’s no standardized way to convey a grab state, we have to cobble together some unconventional methods in the hopes of doing so. Many of the approaches involve manipulating an element’s name, description, or other property.
What we’re testing #
Base HTML markup #
We’ll again use a button as our test element, using the three highlighted approaches from part 1. But unlike part 1, I’m only testing the elements inside of a role="application" parent since that seemed to provide better support. It will also make the testing combinations a bit more manageable.
html snippet: Markup for the three draggable items being used
html<div role="application"> <!-- item type #1 - static text --> <button draggable="true">Apple (draggable)</button> <!-- item type #2 - 'aria-describedby' attribute --> <button draggable="true" aria-describedby="dragDesc">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- item type #3 - 'aria-roledescription' attribute --> <button draggable="true" aria-roledescription="draggable button">Apple</button> </div>
Navigating to items #
As items are within a container with role="application", they’ll be navigated to via custom key functionality that programmatically moves focus on key press.
Grabbing items #
Items will have JavaScript click event handlers that apply any markup changes for that approach’s grab state.
They’ll be “clicked” via the default method for that platform. Clicking will toggle an item’s grab state between released and grabbed.
Tests #
An item’s grab state will be tested in 4 situations:
- When it’s navigated to in a released state
- When it’s navigated to in a grabbed state
- When its grab state changes from released to grabbed
- When its grab state changes from grabbed to released
You may wonder – why also test how the grab state is communicated upon navigation, instead of only when a grab or release occurs? Two reasons:
- Unlike mouse or pure keyboard users, a screen reader’s virtual cursor doesn’t always follow the browser focus. So it’s possible a screen reader user could grab an item and then navigate away from it without either dropping it themselves, or triggering any sort of auto-drop functionality tied to the browser focus. If they were to return to the item, it’d be crucial the grabbed state is still communicated.
- If your application allows for dragging more than one item at a time, users will be able to navigate to and grab multiple items at once. So conveying the grab state is critical.
Testing Scope #
Also like the previous article, I’ll be testing four common desktop and two mobile browser and screen reader combinations. To understand why I chose those particular combinations, refer to the “testing scope” section of part 1.
Desktop #
- JAWS (2023.2306.38) + Chrome (120) on Windows 10
- NVDA (2023.3.0.29780) + Firefox (120) on Windows 10
- Narrator (Windows 10 v22H2) + Edge (120) on Windows 10
- VoiceOver + Safari (17.2) on MacOS 13.6.1
Mobile #
- TalkBack (14.0.1) + Chrome (119) on Android 11, Samsung Galaxy A20s
- VoiceOver + Safari on iOS 17.2, iPhone 13 Pro
Note:
Note: some of the tested approaches add an accessible description to an item. Recently, iOS VoiceOver began adding the word “description” when announcing an item’s description. I find this unnecessary and verbose, though apparently it is intentional, per this WebKit bug report.
This is the reason you’ll notice some of the iOS VoiceOver results include “description” in the recorded announcements.
Keep in mind that a lack of support isn’t necessarily a screen reader issue – it could be due to a browser, or a specific screen reader + browser pairing. For brevity I may refer only to the screen readers, but know that I’m actually referring to both.
The approaches #
Let’s finally dive into this article’s purpose: communicating the grab state of an item. Unlike part 1, there’s not many options available that allow us to do what we’re trying to do. In order to cast a wider net, I tried some out-of-the-box ideas.
This is another reason why drag-and-drop patterns are so tricky to get right.
Static text #
We used static text in part 1 as one of the ways to indicate draggability. It simply places the drag information directly inside the button with the label.
That method was simple and had great support, so why not try it again for the grab state?
When grabbed, static text is added to the button and removed on release. The key will be if screen readers re-announce the item as its content changes.
html snippet: Static text grab state approach
html<!-- draggable item #1 – static text --> <!-- default state --> <button draggable="true">Apple (draggable)</button> <!-- grabbed state --> <button draggable="true">Apple (grabbed)</button> <!-- draggable item #2 – 'aria-describedby' attribute --> <!-- default state --> <button draggable="true" aria-describedby="dragDesc">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- grabbed state --> <button draggable="true" aria-describedby="dragDesc">Apple (grabbed)</button> <div hidden id="dragDesc">draggable</div> <!-- draggable item #3 – 'aria-roledescription' attribute --> <!-- default state --> <button draggable="true" aria-roledescription="draggable button">Apple</button> <!-- grabbed state --> <button draggable="true" aria-roledescription="draggable button">Apple (grabbed)</button>
Note that item #1 already uses static text to indicate its draggability, so we just swap the text when it’s grabbed.
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple grabbed button" | [no announcement] | [no announcement] |
| NVDA + Firefox | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple draggable" |
| Narrator + Edge | "apple draggable button" | "apple grabbed button" | "apple grabbed button" | "apple draggable button" |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple draggable" |
| TalkBack + Android Chrome | "apple draggable button" | "apple grabbed button" | [no announcement] | [no announcement] |
| VoiceOver + iOS Safari | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple draggable" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple button draggable" | "apple grabbed button draggable" | [no announcement] | [no announcement] |
| NVDA + Firefox | "apple button draggable" | "apple grabbed button draggable" | "apple grabbed" | "apple" |
| Narrator + Edge | "apple button draggable" | "apple grabbed button draggable" | "apple grabbed button draggable" | "apple button draggable" |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple" |
| TalkBack + Android Chrome | "apple button draggable" | "apple grabbed button draggable" | [no announcement] | [no announcement] |
| VoiceOver + iOS Safari | "apple description draggable button" | "apple grabbed description draggable button" | "apple grabbed description draggable" | "apple description draggable" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple grabbed draggable button" | [no announcement] | [no announcement] |
| NVDA + Firefox | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple" |
| Narrator + Edge | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed draggable button" | "apple draggable button" |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple" |
| TalkBack + Android Chrome | "apple draggable button" | "apple grabbed draggable button" | [no announcement] | [no announcement] |
| VoiceOver + iOS Safari | "apple button" | "apple grabbed button" | "apple grabbed" | "apple" |
Approach findings #
The support for a static text grab state was mostly good, as every screen reader announced the items correctly on navigation.
But on grab and release there were a couple of large support holes – neither JAWS nor TalkBack announced the updated button text after the action. JAWS + Chrome is the most common screen reader and browser pairing in use, so a lack of support there is a pretty big hole.
We can’t have users activate an item and then hear nothing, so it makes this grab state approach a non-starter.
aria-describedby attribute #
We used the aria-describedby ARIA attribute in part 1 to point to another element containing the draggable information. We could use it here to relay the grab state.
When an item is grabbed, we add the grabDesc id as its aria-describedby value. The grabDesc element exists elsewhere in the document and contains the text “grabbed”. This will add that as the item’s accessible description.
html snippet: aria-describedby grab state approach
html<!-- draggable item #1 – static text --> <!-- default state --> <button draggable="true" aria-describedby="">Apple (draggable)</button> <!-- grabbed state --> <button draggable="true" aria-describedby="grabDesc">Apple (draggable)</button> <!-- draggable item #2 – 'aria-describedby' attribute --> <!-- default state --> <button draggable="true" aria-describedby="dragDesc">Apple</button> <!-- grabbed state --> <button draggable="true" aria-describedby="grabDesc">Apple</button> <!-- draggable item #3 – 'aria-roledescription' attribute --> <!-- default state --> <button draggable="true" aria-describedby="" aria-roledescription="draggable button">Apple</button> <!-- grabbed state --> <button draggable="true" aria-describedby="grabDesc" aria-roledescription="draggable button">Apple</button> <!-- descriptive elements --> <div hidden id="dragDesc">draggable</div> <div hidden id="grabDesc">grabbed</div>
Note that the second draggable item already uses aria-describedby to indicate that it’s draggable. So we just swap the value from one id to the other.
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple draggable button grabbed" | "grabbed" | [no announcement] |
| NVDA + Firefox | "apple draggable button" | "apple draggable button grabbed" | "grabbed" | [no announcement] |
| Narrator + Edge | "apple draggable button" | "apple draggable button grabbed" | [no announcement] | [no announcement] |
| VoiceOver + Safari | "apple draggable button" | "apple draggable grabbed button" | [no announcement] | [no announcement] |
| TalkBack + Android Chrome | "apple draggable button" | "apple draggable button grabbed" | [no announcement] | [no announcement] |
| VoiceOver + iOS Safari | "apple draggable button" | "apple draggable button" | "apple draggable" | "apple draggable" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple button draggable" | "apple button grabbed" | "grabbed" | "draggable" |
| NVDA + Firefox | "apple button draggable" | "apple button grabbed" | "grabbed" | "draggable" |
| Narrator + Edge | "apple button draggable" | "apple button grabbed" | [no announcement] | [no announcement] |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed button" | [no announcement] | [no announcement] |
| TalkBack + Android Chrome | "apple button draggable" | "apple button grabbed" | [no announcement] | [no announcement] |
| VoiceOver + iOS Safari | "apple description draggable button" | "apple description draggable button" | "apple description draggable" | "apple description draggable" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple draggable button grabbed" | "grabbed" | [no announcement] |
| NVDA + Firefox | "apple draggable button" | "apple draggable button grabbed" | "grabbed" | [no announcement] |
| Narrator + Edge | "apple draggable button" | "apple draggable button grabbed" | [no announcement] | [no announcement] |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed draggable button" | [no announcement] | [no announcement] |
| TalkBack + Android Chrome | "apple draggable button" | "apple draggable button grabbed" | [no announcement] | [no announcement] |
| VoiceOver + iOS Safari | "apple button" | "apple button" | "apple" | "apple" |
Approach findings #
The aria-describedby grab state approach also has some big issues.
First, iOS VoiceOver doesn’t appear to support changes to an item’s accessible description, as navigating to the grabbed item still gets the initial description announced. iOS VoiceOver has a 70% mobile screen reader market share, so this is a significant gap.
Next, and just as important, only JAWS and NVDA announced the change to the aria-describedby value on grab and release actions. The other screen readers don’t announce changes to an item’s description.
This is disappointing, as this approach is clean. Placing the grab state in an item’s accessible description means it won’t interfere with selecting or identifying items. It would also translate without issues, since the content exists on the page from the start.
But unless we know that users are only using JAWS or NVDA, this grab state approach won’t work.
Switch (role=”switch” with aria-checked attribute) #
The role="switch" ARIA attribute and value turns an item into a switch component with an on or off state. Screen readers typically announce them like “Apple, switch, on/off”.
A cousin of the checkbox, switches typically perform immediate actions, whereas checkboxes usually require a form submission. You’ve likely encountered switches on device settings pages.
When using a non-checkbox element, the aria-checked attribute is also needed to track the switch’s state. Since we’re using a button element, we’ll need this.
So how might we use it? If a user understands an item is both draggable and a switch, it’s possible they could associate the on and off values with it being either picked up or placed down. It could also just end up being confusing.
html snippet: Switch grab state approach
html<!-- draggable item #1 – static text --> <!-- default state --> <button draggable="true" role="switch" aria-checked="false">Apple (draggable)</button> <!-- grabbed state --> <button draggable="true" role="switch" aria-checked="true">Apple (draggable)</button> <!-- draggable item #2 – 'aria-describedby' attribute --> <!-- default state --> <button draggable="true" aria-describedby="dragDesc" role="switch" aria-checked="false">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- grabbed state --> <button draggable="true" aria-describedby="dragDesc" role="switch" aria-checked="true">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- draggable item #3 – 'aria-roledescription' attribute --> <!-- default state --> <button draggable="true" aria-roledescription="draggable switch" role="switch" aria-checked="false">Apple</button> <!-- grabbed state --> <button draggable="true" aria-roledescription="draggable switch" role="switch" aria-checked="true">Apple</button>
Notice that item #3’s aria-roledescription value uses “draggable switch” for this item instead of “draggable button”.
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable switch off" | "apple draggable switch pressed on" | "pressed on" | "off" |
| NVDA + Firefox | "apple draggable switch off" | "apple draggable switch on" | "on" | "off" |
| Narrator + Edge | "apple draggable switch off" | "apple draggable switch on" | "apple draggable switch on" | "apple draggable switch off" |
| VoiceOver + Safari | "apple draggable off switch" | "apple draggable on switch" | "on apple draggable switch" | "off apple draggable switch" |
| TalkBack + Android Chrome | "off apple draggable switch" | "on apple draggable switch" | "on" | "off" |
| VoiceOver + iOS Safari | "apple draggable checkbox unchecked" | "apple draggable checkbox checked" | "checked" | "unchecked" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple switch off draggable" | "apple switch pressed on draggable" | "pressed on" | "off" |
| NVDA + Firefox | "apple switch off draggable" | "apple switch on draggable" | "on" | "off" |
| Narrator + Edge | "apple switch off draggable" | "apple switch on draggable" | "apple switch on draggable" | "apple switch off draggable" |
| VoiceOver + Safari | "apple draggable off switch" | "apple draggable on switch" | "on apple draggable switch" | "off apple draggable switch" |
| TalkBack + Android Chrome | "off apple switch draggable" | "on apple switch draggable" | "on" | "off" |
| VoiceOver + iOS Safari | "apple description draggable checkbox unchecked" | "apple description draggable checkbox checked" | "checked" | "unchecked" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable switch off" | "apple draggable switch pressed on" | "pressed on" | "off" |
| NVDA + Firefox | "apple draggable switch off" | "apple draggable switch on" | "on" | "off" |
| Narrator + Edge | "apple draggable switch off" | "apple draggable switch on" | "apple draggable switch on" | "apple draggable switch off" |
| VoiceOver + Safari | "apple off switch" | "apple on switch" | "on apple switch" | "off apple switch" |
| TalkBack + Android Chrome | "off apple draggable switch" | "on apple draggable switch" | "on" | "off" |
| VoiceOver + iOS Safari | "apple checkbox unchecked" | "apple checkbox checked" | "apple checked" | "apple unchecked" |
Approach findings #
The support for the switch representing the grab state was decent, with some concerns.
All screen readers tested announced a “draggable switch” with an “on/off” grab state for items #1 and 2, both during navigation and interactions – showing that switches have mostly good support.
iOS VoiceOver was an exception with those items – ignoring the switch role and treating the items as a checkbox. Depending on the situation, it’s possible that users could make sense of a “draggable checkbox”.
Item #3’s aria-roledescription didn’t fully mesh with the switch approach – JAWS, NVDA, Narrator, and TalkBack correctly called it a “draggable switch” with an “on/off” state. But MacOS and iOS VoiceOver called it a “switch” and “checkbox”, respectively, leaving out the important “draggable” indication.
Between items #1 and 2, I like #2 the best with this approach, as it takes some of the positives of the aria-describedby approach and combines them with the switch component.
While flawed, the switch approach has a lot going for it.
Toggle button (aria-pressed attribute) #
We looked at the aria-pressed attribute in part 1 as a potential way to indicate items are draggable. While I concluded that it didn’t work well for that purpose, perhaps it could work better to convey grab state when combined with another method of indicating draggability.
The presence of aria-pressed turns an item into a toggle button, a button with an on or off state. Screen readers typically announce them like “Apple, toggle button, on/off”.
We’d use it here to convey the grab state of a draggable item.
html snippet: Toggle button grab state approach
html<!-- draggable item #1 – static text --> <!-- default state --> <button draggable="true" aria-pressed="false">Apple (draggable)</button> <!-- grabbed state --> <button draggable="true "aria-pressed="true">Apple (draggable)</button> <!-- draggable item #2 – 'aria-describedby' attribute --> <!-- default state --> <button draggable="true" aria-describedby="dragDesc" aria-pressed="false">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- grabbed state --> <button draggable="true" aria-describedby="dragDesc" aria-pressed="true">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- draggable item #3 – 'aria-roledescription' attribute --> <!-- default state --> <button draggable="true" aria-roledescription="draggable toggle" aria-pressed="false">Apple</button> <!-- grabbed state --> <button draggable="true" aria-roledescription="draggable toggle" aria-pressed="true">Apple</button>
Like the previous approach, I change the value of item #3’s aria-roledescription to “draggable toggle” to match the toggle button semantic that the aria-pressed attribute gives.
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable toggle button" | "apple draggable toggle button pressed" | "toggle button pressed apple draggable" | "toggle button not pressed apple draggable" |
| NVDA + Firefox | "apple draggable toggle button not pressed" | "apple draggable toggle button pressed" | "pressed" | "not pressed" |
| Narrator + Edge | "apple draggable toggle button off" | "apple draggable toggle button on" | "apple draggable toggle button on" | "apple draggable toggle button off" |
| VoiceOver + Safari | "apple draggable toggle button" | "apple draggable selected toggle button" | [no announcement] | [no announcement] |
| TalkBack + Android Chrome | "off apple draggable toggle button" | "on apple draggable toggle button" | "on" | "off" |
| VoiceOver + iOS Safari | "apple draggable toggle button not pressed" | "apple draggable toggle button pressed" | "checked" | "unchecked" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple toggle button draggable" | "apple toggle button pressed draggable" | "toggle button pressed apple" | "toggle button not pressed apple" |
| NVDA + Firefox | "apple toggle button not pressed draggable" | "apple toggle button pressed draggable" | "pressed" | "not pressed" |
| Narrator + Edge | "apple toggle button off draggable" | "apple toggle button on draggable" | "apple toggle button on draggable" | "apple toggle button off draggable" |
| VoiceOver + Safari | "apple draggable toggle button" | "apple draggable selected toggle button" | "selected" | [no announcement] |
| TalkBack + Android Chrome | "off apple toggle button draggable" | "on apple toggle button draggable" | "on" | "off" |
| VoiceOver + iOS Safari | "apple description draggable toggle button not pressed" | "apple description draggable toggle button pressed" | "checked" | "unchecked" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable toggle" | "apple draggable toggle pressed" | "toggle button pressed apple" | "toggle button not pressed apple" |
| NVDA + Firefox | "apple draggable toggle not pressed" | "apple draggable toggle pressed" | "pressed" | "not pressed" |
| Narrator + Edge | "apple draggable toggle off" | "apple draggable toggle on" | "apple draggable toggle on" | "apple draggable toggle off" |
| VoiceOver + Safari | "apple toggle button" | "apple selected toggle button" | "selected" | [no announcement] |
| TalkBack + Android Chrome | "off apple draggable toggle" | "on apple draggable toggle" | "on" | "off" |
| VoiceOver + iOS Safari | "apple toggle button not pressed" | "apple toggle button pressed" | "apple pressed" | "apple not pressed" |
Approach findings #
Purely support-wise, the aria-pressed approach worked pretty well. The only blips were with VoiceOver (once again), but those aren’t complete deal breakers.
Similar to the switch approach, only here we get a “toggle button” role announced instead of “switch”. And where the switch states were “on/off”, the toggle button gets a variety of state announcements here: “pressed/not pressed”, “on/off”, and “selected”.
My big issue with the approach is that it’s very wordy – look at item #2’s iOS VoiceOver announcement: “apple description draggable toggle button not pressed”. That’s overly verbose and bound to confuse or irritate users.
Even so, I’d hate to waste an approach that has solid support. So I’m going to pair this approach with item #3, which at least gives us a slightly-shorter “[draggable toggle]” announcement instead of the “[draggable] [toggle button]” of items #1 and 2, a marginal improvement.
And even though VoiceOver doesn’t play well with with the aria-roledescription attribute of item #3, it at least falls back to a regular “toggle button” announcement that could be partially mitigated some with clear instructions.
In terms of translation, the aria-roledescription attribute could be a concern if the tool doesn’t translate attributes. For what it’s worth, a quick test I did with Google Translate did result in the attribute being translated, though a more wide translation test is needed before moving into production.
While not perfect, this method has good enough support and enough positives to rule it a valid approach.
Live region #
A live region is an element that flags itself to screen readers as having dynamic content. So when its content changes, a screen reader can announce it.
There’s a variety of live region types and properties available to developers that I can’t dive fully into here. So if you’d like to learn more, Scott O’Hara’s “Are We Live?” article is a great intro.
Developers frequently use live regions as pseudo screen reader notification centers by injecting content into them as actions occur, which is what we’ll do here to communicate our item’s grab status.
html snippet: Live region grab state approach – HTML
html<!-- live region --> <div id="liveRegion" aria-live="assertive"></div> <!-- live region after grab --> <div id="liveRegion" aria-live="assertive">Apple grabbed</div> <!-- live region after release --> <div id="liveRegion" aria-live="assertive">Apple released</div>
The aria-live attribute denotes the element as a live region, and the assertive value means any updates should be announced immediately, even cutting off other announcements. The live region exists empty on the page initially, then has its content changed dynamically via JavaScript as needed.
js snippet: Live region grab state approach – JavaScript
js// get reference to live region element let liveRegion = document.querySelector('#liveRegion'); // when item is grabbed liveRegion.textContent = "Apple grabbed"; // when item is released liveRegion.textContent = "Apple released";
This differs from the other approaches in that we don’t make any changes to the grabbed item itself. Rather, the live region changes for each announcement we want to make.
An additional step common with live regions that I didn’t show here is to clear it after a handful of seconds. That way, users won’t hear an out-of-context message if they happen to navigate to it later.
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| NVDA + Firefox | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| Narrator + Edge | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| VoiceOver + Safari | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| TalkBack + Android Chrome | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| VoiceOver + iOS Safari | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple button draggable" | "apple button draggable" | "apple grabbed" | "apple released" |
| NVDA + Firefox | "apple button draggable" | "apple button draggable" | "apple grabbed" | "apple released" |
| Narrator + Edge | "apple button draggable" | "apple button draggable" | "apple grabbed" | "apple released" |
| VoiceOver + Safari | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| TalkBack + Android Chrome | "apple button draggable" | "apple button draggable" | "apple grabbed" | "apple released" |
| VoiceOver + iOS Safari | "apple description draggable button" | "apple description draggable button" | "apple apple grabbed" | "apple apple released" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| NVDA + Firefox | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| Narrator + Edge | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| VoiceOver + Safari | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| TalkBack + Android Chrome | "apple draggable button" | "apple draggable button" | "apple grabbed" | "apple released" |
| VoiceOver + iOS Safari | "apple button" | "apple button" | "apple apple grabbed" | "apple apple released" |
Approach findings #
Live regions generally have really good support as long as they’re implemented properly, as was the case here – each grab and release was announced with no issue. There’s a reason they’re a goto of developers for alerting screen readers of actions.
However, our approach here has a huge issue – the items themselves don’t receive any kind of informational update and thus don’t indicate the grab state in any way when navigated to.
So on its own, a live region is kind of useless for us here. So we’d need to combine it with another approach that fill those gaps.
Static text with live region #
We know from earlier that the static text grab state approach had support holes in announcing the text change on grab and release actions. We could potentially fill that gap with a live region.
html snippet: Static text with live region grab state approach – item markup
html<!-- draggable item #1 – static text --> <!-- default state --> <button draggable="true">Apple (draggable)</button> <!-- grabbed state --> <button draggable="true">Apple (grabbed)</button> <!-- draggable item #2 – 'aria-describedby' attribute --> <!-- default state --> <button draggable="true" aria-describedby="dragDesc">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- grabbed state --> <button draggable="true" aria-describedby="dragDesc">Apple (grabbed)</button> <div hidden id="dragDesc">draggable</div> <!-- draggable item #3 – 'aria-roledescription' attribute --> <!-- default state --> <button draggable="true" aria-roledescription="draggable button">Apple</button> <!-- grabbed state --> <button draggable="true" aria-roledescription="draggable button">Apple (grabbed)</button>
The HTML markup is the same as it is earlier. But now there’s also a live region.
html snippet: Static text with live region grab state approach – live region markup
html<!-- live region --> <div id="liveRegion" aria-live="assertive"></div> <!-- live region after grab --> <div id="liveRegion" aria-live="assertive">Apple grabbed</div> <!-- live region after release --> <div id="liveRegion" aria-live="assertive">Apple released</div>
When navigating to an item, the announcement of the item’s text would give the grab state, while the live region would announce the grab state change during interactions.
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple released" |
| NVDA + Firefox | "apple draggable button" | "apple grabbed button" | "apple grabbed apple grabbed" | "apple released apple draggable" |
| Narrator + Edge | "apple draggable button" | "apple grabbed button" | "apple grabbed button apple grabbed" | "apple draggable button apple released" |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple released" |
| TalkBack + Android Chrome | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple released" |
| VoiceOver + iOS Safari | "apple draggable button" | "apple grabbed button" | "apple grabbed" | "apple released" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple button draggable" | "apple grabbed button draggable" | "apple grabbed" | "apple released" |
| NVDA + Firefox | "apple button draggable" | "apple grabbed button draggable" | "apple grabbed apple grabbed" | "apple released apple" |
| Narrator + Edge | "apple button draggable" | "apple grabbed button draggable" | "apple grabbed button draggable" | "apple button draggable apple released" |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple released" |
| TalkBack + Android Chrome | "apple button draggable" | "apple grabbed button draggable" | "apple grabbed" | "apple released" |
| VoiceOver + iOS Safari | "apple description draggable button" | "apple grabbed description draggable button" | "apple grabbed" | "apple released" |
| Browser / screen reader | On navigating to released item | On navigating to grabbed item | On grab | On release |
|---|---|---|---|---|
| JAWS + Chrome | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple released" |
| NVDA + Firefox | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed apple grabbed" | "apple released apple" |
| Narrator + Edge | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed draggable button apple grabbed" | "apple draggable button apple released" |
| VoiceOver + Safari | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple released" |
| TalkBack + Android Chrome | "apple draggable button" | "apple grabbed draggable button" | "apple grabbed" | "apple released" |
| VoiceOver + iOS Safari | "apple button" | "apple grabbed button" | "apple grabbed" | "apple released" |
Approach findings #
We saw earlier that just the static text alone had good support when navigating to the item, but had holes when the actions occurred.
But adding in the live region here seemed to fill the holes for those screen readers that didn’t announce the interactions. All of the screen readers here announced the items correctly in both states as well as on grab/release.
The minor quips are that NVDA and Narrator seem to announce both the button’s text and the live region content, resulting in a double announcement. Not ideal, but not a deal-breaker either.
While this approach’s support is great, there’s some drawbacks.
First, the item’s visual label (also its accessible name) changes with its grab state, which may confuse users. Placing dynamic state information in an item’s label feels dirty to me, regardless of the support.
Next, voice control users that activate elements by name could be affected – they’d need to speak the “grabbed” text (and “draggable” for item #1) to activate items. With MacOS’s Voice Control, “click apple” did not work for me if the button’s text included “grabbed”; I had to speak “click apple grabbed” to activate it.
Lastly, translation services may have issues translating the added “grabbed” text, as it’s not present on page load.
Testing with Google Translate, which translates on page load, “grabbed” was unsurprisingly not translated. I also tested an instant translation tool – the popular “instantTranslate” add-on for NVDA, which did translate the grab state text. So depending on both the user’s translation tool and your development approach, this may or may not be an concern.
Despite these potential drawbacks, the support for this grab state approach is great. Since this approach uses static text for the grab state, I think pairing it with item #1, the static draggable item makes the most sense.
aria-describedby with live region #
Like the static text approach, the aria-describedby grab state approach could conceivably be paired with a live region to fill in some gaps.
But unfortunately, we already know from earlier that iOS VoiceOver doesn’t recognize changes to an item’s accessible description. So even if we filled the lack of announcements on grab and release actions with the live region, we still have that big hole of iOS VoiceOver not announcing an item’s correct grab state when navigating to it.
If this changes, I may retest this approach and update this article, but for now it means that this approach not a possibility.
Accessibility Notification API #
The Accessibility Notification API is a proposed JavaScript API that aims to allow developers to directly pass communication to screen readers. Though it doesn’t exist yet, it might solve some problems here if it did, so I wanted to at least mention it.
Currently, developers are stuck doing what we’re doing in this article – hacking and blending attributes, elements, and live regions in an attempt to keep screen reader users informed of page changes.
This new API’s goal is to provide an easy and consistent way of updating screen readers via an ariaNotify() syntax. Here, we might use it like this:
js snippet: Accessibility Notification API grab state code example
js// when item is grabbed document.ariaNotify("Apple grabbed"); // when item is released document.ariaNotify("Apple released");
It likely wouldn’t be a complete solution, but would certainly open up possibilities for complex interactions like drag-and-drop. If nothing else, it might be a cleaner version of the live region approaches.
Unfortunately, at the time of this writing only an explainer document exists about the Accessibility Notification API. But as of December 2023, it looks like Chromium developers are aiming to prototype it in 2024, but that’s the latest information I could find.
So at best, this approach seems like it’s at least several years away.
Summary #
I’ve explored and tested the support of numerous approaches for conveying the grab state of several draggable items to screen reader users. As I mentioned at the start, I wish there was a proper, clearer way of doing this. But since that’s not the case, I needed to look at some unorthodox methods.
The approaches that stand out to me are:
- The switch grab state approach with the
aria-describedbydraggable item (#2) - The toggle button grab state approach with the
aria-roledescriptiondraggable item (#3) - The static text grab state approach with the static draggable item (#1) paired with a live region.
Switch (role="switch" with aria-checked attribute) #
I felt that the switch grab state approach worked best with the #2 draggable item, which uses the aria-describedby attribute to indicate draggability:
html snippet: Switch grab state approach with draggable item #2
html<!-- draggable item #2 – 'aria-describedby' attribute --> <!-- default state --> <button draggable="true" aria-describedby="dragDesc" role="switch" aria-checked="false">Apple</button> <div hidden id="dragDesc">draggable</div> <!-- grabbed state --> <button draggable="true" aria-describedby="dragDesc" role="switch" aria-checked="true">Apple</button> <div hidden id="dragDesc">draggable</div>
This approach had solid support, with all but one of the screen readers announcing the item as a “switch” with an “on|off” state when navigated to. They also announced updates when changed.
iOS VoiceOver was the outlier, falling back to calling it a “checkbox” with “checked|unchecked” values.
There won’t be any translation issues, as the “draggable” text is in a separate element that’s present on page load, and the control and state information for the switch are part of the screen reader and would be given in the user’s chosen language.
The key for this approach working for users is if they’ll be able to make sense of the switch’s value reflecting the grabbed or released state of the item.
I’ll be adding this approach to the prototypes I test in a future article.
Toggle button (aria-pressed attribute) #
The toggle button grab state approach that resonated with me the most was with draggable item #3, which uses the aria-roledescription attribute for indicating draggability:
html snippet: Toggle button grab state approach with draggable item #3
html<!-- draggable item #3 – 'aria-roledescription' attribute --> <!-- default state --> <button draggable="true" aria-roledescription="draggable toggle" aria-pressed="false">Apple</button> <!-- grabbed state --> <button draggable="true" aria-roledescription="draggable toggle" aria-pressed="true">Apple</button>
This approach had good support, with most screen readers announcing the “draggable toggle” with an “on|off” or “pressed|not pressed” state when navigated to.
VoiceOver again didn’t cooperate fully, with both flavors of it ignoring the aria-roledescription attribute and falling back to a “toggle button”. While not idea, we can potentially mitigate this in production if we can teach users ahead of time.
As far as translations go, it looked like the aria-roledescription value was translated in my test, but it’s worth further testing. And like the switch, the screen reader will announce the toggle button’s information in the user’s language setting.
This approach also results in some verbose announcements, so that’s something to keep an eye on when testing with users.
This approach will also be added to the list of prototypes in another article.
Static text with live region #
The last approach I’ll be advancing forward with is the combination of the static grab text with a live region. This approach worked best with the static draggable item (#1):
html snippet: Static text grab state approach with item #1
html<!-- draggable item #1 – static text --> <!-- default state --> <button draggable="true">Apple (draggable)</button> <!-- grabbed state --> <button draggable="true">Apple (grabbed)</button> <!-- live region --> <div id="liveRegion" aria-live="assertive"></div> <!-- live region after grab --> <div id="liveRegion" aria-live="assertive">Apple grabbed</div> <!-- live region after release --> <div id="liveRegion" aria-live="assertive">Apple released</div>
This approach had great support, with all screen readers announcing the correct grab state on navigation, as well as receiving the correct action announced on grab and release.
Despite the good support, there’s some concerns. First, using the item’s label to hold dynamic state information goes against best practices, even if it does work. Whether that trips up users in identifying or activating items remains to be seen.
There’s also potential for translation issues, but that’s dependent on the user’s preferred tooling, as well as the way the interface is implemented.
Nevertheless, this approach will be prototyped with the other two grab state approaches.
Conclusion #
Through two articles, we’ve now looked at a variety of ways to identify items as draggable, and how to communicate when they’ve been grabbed and released. We’ve identified three approaches that will be built into prototypes and tested with users.
It’s worth restating that many drag-and-drop scenarios are different from one another, so the simplified functionality I look at here may not apply in every situation.
But before we do that, I need to tackle one more task: how to communicate to a user when a draggable item has been moved. I hope to get it up in a few months, but in the meantime, feel free to message or email me with comments or questions. Thanks for reading!