Free-Shipping Progress Bars: The Threshold Message Screen Readers Never Hear
A shopper adds an item to the cart. The free-shipping bar fills to 60 percent, and the text says 24 dollars away from free shipping. A sighted shopper sees it instantly. A screen reader user hears nothing, because the bar is a div with a width style, and the message is text that is never announced. The bar is a status update that only half the shoppers receive.
A progress bar is a status, and the W3C's ARIA27 technique says the status of a progress bar should be communicated through an accessible name and a live region, not through visual width alone. The bar needs a role of progressbar, with aria-valuenow, aria-valuemin, and aria-valuemax, so the screen reader can report the percentage. The threshold message, the text that says how far away free shipping is, needs a live region, so the screen reader announces it when it changes.
The bar is a status, not a decoration
The mistake is treating the progress bar as a visual decoration. It is a status indicator, and status indicators have to be announced. A div with a width of 60 percent is invisible to a screen reader, because the screen reader does not see the width. It sees a div, and a div has no semantics. The fix is to give the bar a role and a value, and to put the threshold message in a live region that is empty on load, so the screen reader announces the change when it happens, not when the page loads.
Sara Soueidan's work on live regions is the reference here. A live region with role status is polite by default, which means the screen reader will announce the change when it finishes its current utterance, not interrupt it. An aria-live region with aria-atomic set to true will announce the whole message, not just the part that changed, which matters for a threshold message, because the number and the words together are the message. The output element is the native HTML element for this, because it has an implicit status role, and it is the element most developers forget exists.
The threshold message is the content
The bar itself is not the content. The message is the content, and the message is what the shopper needs: how much more to spend, and what happens when they reach it. A message that says 24 dollars away from free shipping is useful. A message that says 60 percent is not, because the shopper does not know what 60 percent means in dollars. The message should state the amount and the threshold, in words, and it should be announced when it changes, not when the page loads.
Test it with a screen reader, not a checklist. Add an item to the cart, and listen. If the screen reader says nothing, the bar is broken, even if the automated checker passes it. If the screen reader announces the threshold message when it changes, and the percentage when the bar moves, the bar works. The test takes two minutes, and it is the only test that matters.
The fix, in order
Give the bar a role of progressbar, with aria-valuenow, aria-valuemin, and aria-valuemax. Put the threshold message in a live region with role status, empty on load, so the screen reader announces the change. Write the message in dollars and words, not percentages. Test it with a screen reader, adding and removing items, and listen for the announcement. If it announces, the bar works. If it does not, the bar is a decoration, and a decoration is not a status.
The free-shipping bar is a conversion tool, and it only converts the shoppers who can perceive it. Make it perceivable, and it converts everyone.
Keep reading: Cart Live Regions: When a Polite Announcement Arrives Too Late, Quantity Steppers That Announce Themselves: Live Regions in the Cart, Promo code errors in the cart: announce the failure, keep the code, say what was wrong.
Frequently asked questions
How should a free-shipping progress bar work for screen readers?
Give the bar a role of progressbar with aria-valuenow, aria-valuemin, and aria-valuemax, and put the threshold message in a live region with role status, empty on load, so the screen reader announces it when it changes.
What is the difference between a polite and an assertive live region?
A polite live region, role status, waits for the screen reader to finish its current utterance. An assertive live region interrupts. For a threshold message, polite is right, because the shopper does not need to be interrupted.
What is the output element, and why does it matter?
The output element is the native HTML element for status messages, with an implicit status role. It is the element most developers forget exists, and it is the simplest way to make a threshold message announceable.
Why does my progress bar pass the automated checker but fail for screen readers?
Because automated checkers test for the presence of a role, not whether the status is announced. A div with a width style is invisible to a screen reader, and a message that is not in a live region is never announced.