Announcing Numeric Changes with Direction and Magnitude
Permalink to "Announcing Numeric Changes with Direction and Magnitude"A KPI tile changes from 1,240 to 1,310 and turns green with an up arrow. Sighted users read “up, a little” in a glance. A screen reader user hears either nothing or “1,310”, with no sense of whether that is good, bad, big or small. Useful announcements state the new value, the direction in words and the size of the change: “Open orders: 1,310, up 70 (5.6%) since 10:00”. They are also rare enough to be heard: only changes that cross a threshold, coalesced and rate-limited.
This guide belongs to ARIA live regions for dynamic data.
Spec reference
Permalink to "Spec reference"- SC 4.1.3 Status Messages (AA) — status messages announced without focus.
- SC 1.4.1 Use of Color (A) — direction not conveyed only by green and red.
- SC 1.1.1 (A) — arrow icons need text equivalents (or are decorative when text conveys direction).
- SC 2.2.2 Pause, Stop, Hide (A) — auto-updating information must be controllable.
When to use — and when not to
Permalink to "When to use — and when not to"Announce changes that users are monitoring and that matter for their task: a queue length crossing a limit, an error rate rising, a balance changing after their own action. Pair with a pause control and a setting to choose which metrics speak.
Do not announce every tick of a fast-changing value; it drowns everything else. The misapplication: wrapping a whole KPI row in aria-live="polite", so every refresh reads six numbers with no direction or context.
Annotated code example
Permalink to "Annotated code example"const fmt = new Intl.NumberFormat('en-GB');
const pct = new Intl.NumberFormat('en-GB', { style: 'percent', maximumFractionDigits: 1, signDisplay: 'never' });
const lastSpoken = new Map();
function maybeAnnounce(metric, value) {
const prev = lastSpoken.get(metric.id) ?? metric.baseline;
const delta = value - prev, ratio = prev ? delta / prev : 0;
if (Math.abs(ratio) < metric.threshold) return; // below threshold: display only
const dir = delta > 0 ? 'up' : 'down';
const judgement = metric.goodWhen === dir ? '' : ' — attention'; // optional meaning, in words
queue.push(`${metric.name}: ${fmt.format(value)}, ${dir} ${fmt.format(Math.abs(delta))} (${pct.format(ratio)})${judgement}`);
lastSpoken.set(metric.id, value);
}
<div class="kpi">
<h3 id="k-orders">Open orders</h3>
<p><span class="value">1,310</span>
<span class="delta"><svg viewBox="0 0 20 20" aria-hidden="true">…▲…</svg> up 70 (5.6%) since 10:00</span></p> <!-- visible words too -->
</div>
<div role="status" class="sr-only" id="kpi-announcer"></div>
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Situation | Announcement |
|---|---|
| Small change under threshold | None; display updates |
| Threshold crossed | “Open orders: 1,310, up 70 (5.6%)” |
| Several metrics cross together | Queued, one per message, most important first |
| Announcements paused | None; a visible “Paused” state |
| User focuses a KPI | Full value and change read from the tile |
Step-by-step implementation
Permalink to "Step-by-step implementation"- Pick metrics that may speak. Not every number on a dashboard deserves an announcement. Choose the few users actively monitor, and make the list user-configurable.
- Set thresholds per metric. A 5% change in revenue might matter; a 5% change in page views might not. Allow absolute thresholds for small numbers, where percentages exaggerate.
- Format for listening. Name, new value, direction word, absolute change, percentage, and time base if relevant. Round to the precision users care about.
- Coalesce and limit. Use a queue that keeps only the latest message per metric and spaces messages by several seconds.
- Give control. Provide “Pause announcements” and per-metric toggles, and remember the choices.
Integration context
Permalink to "Integration context"Live region basics are in choosing between polite and assertive aria-live regions and queuing in announcement queues for competing messages. Streaming data is covered in ticker and price feed announcements, and user control in verbosity settings for data announcements.
Gotchas
Permalink to "Gotchas"Percentages of zero. A change from 0 has no percentage; say “up 12 from zero”.
Good and bad. Up is not always good (error rate). If you convey judgement, use words like “attention” rather than relying on colour.
Locales. Format numbers and percentages with the user’s locale; “1.310” means something different in Norwegian and English.
Edge cases
Permalink to "Edge cases"- Oscillating values. A value bouncing around a threshold triggers repeated messages; add hysteresis so it must move back by a margin before re-announcing.
- Hidden tabs. When the page is hidden, stop announcing and summarise on return: “While away: open orders up 210”.
- User-caused changes. After the user’s own action, announce the result immediately, ignoring thresholds.
Worked example
Permalink to "Worked example"A support operations dashboard updated six KPI tiles every five seconds, each wrapped in a polite live region. Screen reader users heard a constant stream of numbers and turned the screen reader off. The team removed the live regions from tiles, added a single announcer with per-metric thresholds, direction words and a 30-second rate limit, and a settings panel to choose metrics. Agents using NVDA kept announcements on for queue length and SLA breaches only and said the dashboard became usable for the first time.
Common mistakes
Permalink to "Common mistakes"- Live regions around every tile.
- Raw numbers with no direction.
- Colour as the only good/bad cue.
- No pause or settings.
Testing checklist
Permalink to "Testing checklist"Testing it with a screen reader
Permalink to "Testing it with a screen reader"Run the dashboard with simulated data that changes every second. With NVDA, sit on the page for five minutes and count announcements; there should be only a handful, each understandable on its own. Push one metric past its threshold and confirm the message wording, then make it oscillate to verify hysteresis. Toggle “Pause announcements” and confirm silence. Repeat with VoiceOver, which can drop messages that arrive too quickly, and check that the queue spaces them enough to be heard.
Writing announcement text
Permalink to "Writing announcement text"Read every message aloud before shipping it. Spoken numbers are slower than read ones, so drop unnecessary precision: “1.3 million” is quicker than “1,312,408”. Put the metric name first, so a user who only catches the start still knows what changed. Use the same direction words everywhere — “up” and “down”, or “increased” and “decreased” — rather than mixing synonyms. And keep a time base where it matters: “up 70 since 10:00” is clearer than “up 70” when updates are irregular.
Design system notes
Permalink to "Design system notes"Provide a KPI component with an announce policy (off, threshold, always) and a shared announcer service, rather than live regions inside each tile.
FAQ
Permalink to "FAQ"Should KPI tiles be live regions?
No. Use one shared announcer and keep tiles as static content.
Polite or assertive?
Polite, except for critical alerts the user asked for.
How often is too often?
More than one message every few seconds is too often for most users.
Should I announce percentage or absolute change?
Both when both are meaningful; absolute alone for small numbers.
How do I convey good or bad?
In words (“attention”, “within target”), never by colour alone.
What if the user is typing?
Polite messages wait; still keep them rare so they do not pile up.
Related
Permalink to "Related"- Ticker and price feed announcements — high-frequency data
- Verbosity settings for data announcements — user control
- Choosing polite or assertive — urgency