Formatting Durations for Screen Readers
Permalink to "Formatting Durations for Screen Readers"Durations are everywhere in data UIs: response times, session lengths, build times, SLA countdowns, call durations. Visually they are compact: “01:23:45”, “1h 23m”, “83 min”. Read aloud by a screen reader, those forms go wrong in predictable ways. “01:23:45” is read as a time of day (“one twenty-three forty-five”); “1h 23m” may become “1 h 23 m” or “1 hour 23 meters”; “1.5h” may be read as “one point five h”. This guide shows how to keep compact visual forms while giving screen readers text that sounds right.
This guide belongs to accessible date & time data.
Spec reference
Permalink to "Spec reference"- SC 1.3.1 Info and Relationships (A) — the meaning (a duration of 1 hour 23 minutes) must be programmatically determinable, not just visually implied.
- SC 3.1.4 Abbreviations (AAA) — “h”, “m”, “s” are abbreviations; expanding them helps all users and is easy.
- HTML
timeelement —datetimeaccepts a duration string such asPT1H23M45S. Screen readers read the text content, so the attribute is for machines. Intl.DurationFormat— formats durations with localised units and plurals inlong,short,narrowanddigitalstyles (Baseline 2025).
When to use — and when not to
Permalink to "When to use — and when not to"Use compact visual formats in dense tables — they align well and scan quickly — and provide long-form text for assistive technology. In headers, state the unit when a column uses one unit throughout (“Response time (ms)”), and then cells can be plain numbers that read correctly.
Do not use the digital “hh:mm:ss” format for durations without a spoken alternative; it is indistinguishable from a time of day. Do not hide the long form in a title attribute.
The misapplication: wrapping every duration in aria-label on a span. Many screen readers ignore aria-label on generic elements, so the label is never read.
Annotated code example
Permalink to "Annotated code example"<!-- Compact visible form; long form for AT; ISO duration for machines (SC 1.3.1) -->
<td>
<time datetime="PT1H23M45S">
<span aria-hidden="true">1h 23m 45s</span>
<span class="visually-hidden">1 hour, 23 minutes, 45 seconds</span>
</time>
</td>
<!-- Same unit for the whole column: unit in the header, numbers in cells -->
<th scope="col">Response time (milliseconds)</th>
<td>184</td>
// Long and narrow forms from one value
const long = new Intl.DurationFormat(navigator.language, { style: 'long' });
const narrow = new Intl.DurationFormat(navigator.language, { style: 'narrow' });
const d = { hours: 1, minutes: 23, seconds: 45 };
narrow.format(d); // "1h 23m 45s"
long.format(d); // "1 hour, 23 minutes, 45 seconds"
// ISO 8601 duration for the datetime attribute
const iso = ({ hours = 0, minutes = 0, seconds = 0 }) => `PT${hours ? hours + 'H' : ''}${minutes ? minutes + 'M' : ''}${seconds ? seconds + 'S' : ''}`;
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Content | NVDA | JAWS | VoiceOver |
|---|---|---|---|
| “01:23:45” alone | “1 23 45” or “1:23:45” | Clock-time reading | “1:23:45” |
| “1h 23m” alone | “1 h 23 m” | “1 h 23 m” | “1 hour 23 minutes” (sometimes) |
| Hidden long form | “1 hour, 23 minutes, 45 seconds” | Same | Same |
| aria-label on span | Often ignored | Often ignored | Sometimes read |
Integration context
Permalink to "Integration context"Durations sit beside timestamps; markup for those is in the time element and relative timestamps. The header-unit principle is the same as in formatting numbers and units in table cells. Live countdowns (SLA timers) are also auto-updating content — see pause, stop, hide controls for auto-updating data.
Gotchas
Permalink to "Gotchas"Sorting. Sort durations by their numeric value (seconds), never by the displayed string.
Rounding. “1 hour” for 59 minutes 31 seconds is misleading in SLA contexts. Choose precision per column and keep it consistent.
Countdowns. A ticking SLA countdown must not be in a live region. Announce thresholds (“5 minutes remaining”) once, if at all.
Framework notes
Permalink to "Framework notes"Intl.DurationFormat is available in current browsers; for older targets, the polyfill @formatjs/intl-durationformat works. date-fns formatDuration gives long English forms with locale support. In Angular, a pure pipe wrapping Intl.DurationFormat keeps templates clean; in React, a <Duration seconds={n} /> component can render the visible and hidden forms together.
function Duration({ seconds }) {
const parts = { hours: Math.floor(seconds / 3600), minutes: Math.floor(seconds % 3600 / 60), seconds: seconds % 60 };
return (<time dateTime={iso(parts)}>
<span aria-hidden="true">{narrow.format(parts)}</span>
<span className="visually-hidden">{long.format(parts)}</span>
</time>);
}
Worked example
Permalink to "Worked example"A CI dashboard listed build times as “00:04:12” and test times as “38s”. A screen reader user heard “four twelve” for builds and “38 s” for tests, and could not tell whether “four twelve” meant a time of day. The redesign keeps the compact forms visually, adds hidden long forms (“4 minutes, 12 seconds”), moves single-unit columns to header units (“Test time (seconds)”), and marks failures that exceeded the time budget with the words “over budget”, not just a red value.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Provide a Duration component and a column type that formats with Intl.DurationFormat, renders the hidden long form and the ISO attribute, and sorts by seconds. Teams then never hand-format durations.
FAQ
Permalink to "FAQ"Can I use aria-label to give the long form?
Not on a span or time element reliably; aria-label is ignored on generic elements by several screen readers. Use visually hidden text.
Is "1h 23m" acceptable if VoiceOver reads it correctly?
Other readers do not. Provide the long form for consistency.
How should negative durations be shown?
In words: “5 minutes overdue” or “5 minutes early”. A minus sign is easy to miss visually and aurally.
Should durations in exports be long form?
Export the numeric value in a base unit (seconds) with the unit in the header; spreadsheets can format it.
What about very long durations?
Switch units: “3 days, 4 hours” rather than “76 hours”. Choose the two most significant units.
Does the same problem affect percentages and ratios?
Similar ones, yes. “3:1” may be read as a time (“three one”), and “50%” is usually fine. For ratios, write “3 to 1” in accessible text, as contrast-ratio tables on this site do.
Should average durations show decimals?
Avoid “1.5 hours” in speech-critical places; “1 hour 30 minutes” is unambiguous. In dense numeric tables with a unit in the header, decimals are fine: “Average handling time (minutes): 12.5”.
Related
Permalink to "Related"- The time element and relative timestamps — times rather than durations
- Formatting numbers and units — units in headers
- Accessible KPI cards and sparklines — durations as headline metrics