Announcing Dashboard Refreshes
Permalink to "Announcing Dashboard Refreshes"Dashboards refresh: on a timer, when a filter changes, when the user presses Refresh. If each widget fetches and announces independently, a filter change produces a dozen overlapping messages — “Revenue updated”, “Orders updated”, “Chart updated” — of which the screen reader speaks one or two, chosen by timing. If nothing announces, users have no idea the numbers under their cursor have changed.
This page coordinates refreshes into one dashboard-level event, summarises what actually changed, and gives users control over automatic refresh. It belongs to data dashboards.
Spec reference
Permalink to "Spec reference"- SC 4.1.3 Status Messages (AA) — a refresh result is a status message; it must be announced without moving focus.
- SC 2.2.2 Pause, Stop, Hide (A) — automatically updating information that starts automatically and is presented alongside other content must be pausable, stoppable or hideable (or its frequency controllable), unless it is essential. Auto-refreshing dashboards fall under this.
- SC 2.2.1 Timing Adjustable (A) — if refreshes remove content (for example, rows disappearing from a “last 15 minutes” list), users may need more time.
- SC 3.2.5 Change on Request (AAA) — changes of context only on user request; a useful target for dashboards that currently re-order or replace widgets on refresh.
The mechanics use the page’s shared status and alert regions and, for coordination, a promise that settles when all widget fetches for a refresh have finished.
When to announce a refresh — and how much
Permalink to "When to announce a refresh — and how much"Announce every user-initiated refresh — a filter change or a Refresh button — because the user is waiting to know it worked. Summarise the result: “Updated for Region: North. Revenue €410k, down 3%. 2 widgets changed.”
For automatic refreshes, announce only meaningful changes, and let users choose: many prefer silence with a visible “Updated 10:05” timestamp they can read when they want, plus alerts for thresholds. Offer settings for off, alerts only, and summaries.
The misapplication to name is each widget component calling announce('Updated') after its own fetch. With twelve widgets, users hear a burst of identical words — or only the last one — and never learn which numbers moved.
Annotated code example
Permalink to "Annotated code example"// Dashboard coordinator: widgets register fetchers; the coordinator speaks
const widgets = new Map(); // id -> { title, fetch, last, significant(prev, next) }
let autoRefresh = loadPref('dash-refresh') ?? 'alerts'; // 'off' | 'alerts' | 'summary'
let timer;
export function register(id, spec) { widgets.set(id, spec); }
export async function refresh({ reason }) { // reason: 'user' | 'timer' | 'filter'
const results = await Promise.allSettled(
[...widgets].map(async ([id, w]) => [id, await w.fetch()]));
const changes = [], failures = [], alerts = [];
for (const r of results) {
if (r.status === 'rejected') { failures.push(r.reason.widgetTitle); continue; }
const [id, next] = r.value;
const w = widgets.get(id);
if (w.alert?.(next)) alerts.push(w.alert(next));
if (w.last !== undefined && w.significant(w.last, next)) changes.push(w.describe(w.last, next));
w.last = next;
w.render(next); // update in place: no remount, focus kept
}
setUpdatedAt(new Date()); // visible "Updated 10:05"
alerts.forEach((a) => announce(a, 'assertive')); // thresholds: always
const speak = reason !== 'timer' || autoRefresh === 'summary';
if (!speak) return;
const parts = [];
if (changes.length) parts.push(changes.slice(0, 3).join('. '));
if (changes.length > 3) parts.push(`${changes.length - 3} more widgets changed`);
if (!changes.length) parts.push('No significant changes');
if (failures.length) parts.push(`Could not update ${failures.join(', ')}`);
announce(`Dashboard updated. ${parts.join('. ')}.`); // SC 4.1.3: one message
}
// SC 2.2.2: users control automatic refresh
export function setAutoRefresh(mode, everyMs = 60000) {
autoRefresh = mode; savePref('dash-refresh', mode);
clearInterval(timer);
if (mode !== 'off') timer = setInterval(() => refresh({ reason: 'timer' }), everyMs);
}
<div class="dash-refresh" role="group" aria-label="Refresh">
<button type="button" onclick="refresh({ reason: 'user' })">Refresh now</button>
<label for="auto">Automatic refresh</label>
<select id="auto">
<option value="off">Off</option>
<option value="alerts" selected>On, announce alerts only</option>
<option value="summary">On, announce summaries</option>
</select>
<p>Updated <time id="updated-at" datetime="2026-03-18T10:05">10:05</time></p>
</div>
significant(prev, next) is a product decision per widget — a revenue change of more than 1%, a new item in the top five, a status change — and it is the heart of a useful summary. Without it, every refresh “changes” every widget.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Trigger | Expected behaviour | Announcement |
|---|---|---|
| Change region filter to North | All widgets refresh in place | “Dashboard updated. Revenue €410k, down 3%. Top account changed to Litware.” |
| Press Refresh, nothing changed | Timestamp updates | “Dashboard updated. No significant changes.” |
| Auto-refresh (alerts only) | Silent update; timestamp changes | Only threshold alerts, assertive |
| One widget fails | Other widgets update | “…Could not update Margin.” |
| Focus in a widget table during refresh | Focus stays on the same cell | Cell value re-read only if the user moves |
Integration context
Permalink to "Integration context"The coordinator’s messages go through the page’s queue from announcement queues for competing messages, with alerts at urgent priority and summaries at high (user-initiated) or low (automatic). Continuous feeds inside a dashboard widget — a live ticker — follow real-time data stream announcements and should not also report through the refresh summary.
KPI values updated in place need atomic structure so a re-read gives the full sentence; see accessible KPI cards and sparklines.
Gotchas
Permalink to "Gotchas"Re-mounting widgets. Refreshing by re-rendering widgets with new keys destroys focus and scroll position inside them. Update data in place.
Refresh during interaction. If the user is typing in a widget’s filter or editing, defer that widget’s refresh until they finish.
Staggered timers. Widgets with different refresh intervals produce frequent small refreshes. Align them to a dashboard-wide interval, or treat each as a separate low-priority event.
Design system notes
Permalink to "Design system notes"Provide a dashboard coordinator service and a widget contract: each widget registers fetch, render, significant and describe, and never announces on its own. The dashboard shell renders the refresh controls, the timestamp and the auto-refresh preference, so every dashboard offers the same SC 2.2.2 control.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"How should a dashboard announce that it has refreshed?
With one message after all widgets have finished, summarising only the changes that matter — for example “Dashboard updated. Revenue down 3 percent. Top account changed.” Widgets should not each announce their own refresh.
Does an auto-refreshing dashboard need a pause control?
Under SC 2.2.2, automatically updating content shown alongside other content must be pausable, stoppable or controllable, unless it is essential. Offer a setting to turn automatic refresh off or change its frequency.
Should automatic refreshes be announced at all?
Often not by default. A visible timestamp plus announced threshold alerts is a good default; let users opt into spoken summaries.
What if a widget fails to refresh?
Keep its previous data visible with a clear stale indicator, and mention the failure in the summary so users know that widget is out of date.
Related
Permalink to "Related"- Announcement queues — merging widget messages
- Real-time data stream announcements — continuous feeds
- Accessible KPI cards — the values that change