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 time element — datetime accepts a duration string such as PT1H23M45S. Screen readers read the text content, so the attribute is for machines.
  • Intl.DurationFormat — formats durations with localised units and plurals in long, short, narrow and digital styles (Baseline 2025).
What screen readers say for common duration formats Comparison of compact formats and how they are often spoken against long-form text that is spoken correctly. What screen readers say for common duration formats✗ Compact format"01:23:45" read as a clock time"1h 23m" read as "1 h 23 m""83 min" read as "83 min" or "83 minimum""1.5h" read as "one point five h""-00:05:00" read as "minus five"✓ Accessible text"1 hour, 23 minutes, 45 seconds""1 hour 23 minutes""83 minutes""1 hour 30 minutes""5 minutes overdue"

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.

Choosing a duration format Flow: is the unit the same for the whole column? Put it in the header and show numbers. Mixed magnitudes? Use a compact visual form plus visually hidden long text. Need machine readability? Add a time element with an ISO duration. Choosing a duration formatSame unit for the column?"Response time (ms)"cells: plain numbersMixed magnitudes?seconds to hourscompact visible + hidden long formMachine-readable needed?sorting, export, microdatatime datetime="PT1H23M"Negative or overdue?countdowns, SLAswords: "5 minutes overdue"

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
Speech length by format Bar chart of approximate syllables spoken for one duration: compact digital form about 6 but ambiguous, narrow units about 6 but read as letters, long form about 14 and unambiguous. Speech length by formatDigital 01:23:45short but read as a clock timeNarrow 1h 23m 45sunits read as lettersLong form"1 hour, 23 minutes, 45 seconds"
Long forms cost a little more speech and remove all ambiguity.

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”.

Permalink to "Related"

← Back to Accessible Date & Time Data