Accessible KPI Cards and Sparklines
Permalink to "Accessible KPI Cards and Sparklines"A KPI card is the smallest unit of a dashboard: “Revenue — €1.28M — ▲ 4.2% — [tiny line]”. Visually it works in a glance. Read by a screen reader, a typical implementation gives “Revenue, 1.28M, 4.2%, graphic” — no unit spoken properly, no direction (the arrow was an icon, the green was colour), no comparison period, and an unlabelled image. The user knows a number exists and little else.
This page makes each part of the card carry its meaning in text, and gives sparklines an honest alternative. It belongs to data dashboards.
Spec reference
Permalink to "Spec reference"- SC 1.1.1 Non-text Content (A) — the sparkline and any trend arrow need a text alternative or must be marked decorative if the same information is in text.
- SC 1.4.1 Use of Color (A) — “green = up, red = down” must also be conveyed another way.
- SC 1.3.1 Info and Relationships (A) — the label, value and change belong together; a heading plus a paragraph expresses that.
- SC 1.4.11 Non-text Contrast (AA) — the sparkline’s line, if it conveys information, needs 3:1.
An SVG sparkline can take role="img" with an aria-label; a decorative one takes aria-hidden="true". aria-describedby can link the card’s value to a longer description if needed.
When to include a sparkline — and when to describe it
Permalink to "When to include a sparkline — and when to describe it"Include a sparkline when the trend matters as much as the current value — a metric that is stable versus volatile, or recovering after a dip. Give it a trend summary as its text alternative: direction, notable points, the period covered.
If the card’s text already says everything the sparkline shows (“up 4.2% on February, highest since June”), mark the sparkline decorative with aria-hidden="true" and let the text carry it. Duplicating the same information as an image alternative adds noise.
The misapplication to name is an aria-label on the sparkline such as “sparkline” or “trend chart”. That tells users an image exists without telling them anything it shows; it is worse than hiding it.
Annotated code example
Permalink to "Annotated code example"<article class="kpi" aria-labelledby="kpi-rev-h">
<h2 id="kpi-rev-h">Revenue</h2>
<p class="kpi-value">
<!-- SC 1.3.1: value with its unit in text -->
<span aria-hidden="true">€1.28M</span>
<span class="visually-hidden">1.28 million euros</span>
</p>
<p class="kpi-change kpi-change--up">
<!-- SC 1.4.1: direction in words; arrow and colour decorate -->
<span aria-hidden="true">▲</span>
<span class="visually-hidden">up</span> 4.2%
<span class="kpi-period">vs February</span>
</p>
<!-- SC 1.1.1: sparkline with a trend summary as its name -->
<figure class="kpi-spark">
<canvas role="img" width="120" height="32"
aria-label="Revenue over the last 12 months: rising, dip in March, peak in November"></canvas>
</figure>
<a href="/reports/revenue" class="kpi-link">Revenue report<span class="visually-hidden"> with full chart and table</span></a>
</article>
// Generate the sparkline's alternative from the same series it draws
function sparkSummary(label, points, periodText) {
const first = points[0].v, last = points.at(-1).v;
const max = points.reduce((a, b) => (b.v > a.v ? b : a));
const min = points.reduce((a, b) => (b.v < a.v ? b : a));
const dir = last > first * 1.02 ? 'rising' : last < first * 0.98 ? 'falling' : 'flat';
return `${label} over ${periodText}: ${dir}, low in ${min.label}, peak in ${max.label}`;
}
On refresh, the value, change and sparkline summary are updated in place. Whether to announce the change is the dashboard’s decision, made once for all cards, as in announcing dashboard refreshes; an individual card should not announce itself.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Navigation | Expected announcement |
|---|---|
H to the card |
“Revenue, heading level 2” |
Down Arrow (browse) |
“1.28 million euros” |
Down Arrow |
“up 4.2% vs February” |
Down Arrow |
“Revenue over the last 12 months: rising, dip in March, peak in November, image” |
Tab |
“Revenue report with full chart and table, link” |
The card is not focusable itself; its only tab stop is the link. Making the whole card a link or button is common, but then its accessible name becomes the entire card text, read on every focus — keep the link separate and short.
Integration context
Permalink to "Integration context"Number formatting — abbreviations, signs, units — follows formatting numbers and units in table cells; the same speech problems apply to cards. Cards sit in the heading structure from landmarks and headings for dashboards.
A sparkline is a good candidate for a short sonification — a one-second sweep that says “volatile” or “steady” — as an optional extra; see sonifying time-series charts.
Gotchas
Permalink to "Gotchas"“Good” direction varies. Up is good for revenue and bad for costs or error rates. Colour the change by meaning (green for good), but always state the direction in words — “up” or “down” — never “positive”.
Rounded values. “€1.3M” and “up 4%” are fine visually; the hidden text can carry the precise value if users need it, or the card can link to it.
Card click targets. Whole-card links are convenient for pointer users. If you add one, keep a separate text link for keyboard users and give the card link a short name.
Design system notes
Permalink to "Design system notes"A KPI card component should take structured inputs — label, value, unit, change, direction, period, series — and generate both the visual and the spoken forms, including the sparkline summary. Taking a pre-formatted string (“€1.28M ▲4.2%”) makes the accessible version impossible to produce.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"How should a KPI card's change indicator be made accessible?
Write the direction in words — “up 4.2 percent compared with February” — and hide the arrow icon from assistive technology. Colour can reinforce good or bad but must not be the only signal.
What alternative text should a sparkline have?
A short summary of the trend it shows — direction, notable highs or lows, and the period — generated from the same data. If the card text already says this, hide the sparkline instead.
Should a whole KPI card be a link?
It can be for pointer users, but its accessible name then includes all the card text. Provide a short, separate link for keyboard and screen reader users, and keep the card itself readable as plain content.
How should a KPI card show that a value is stale or failed to load?
In text, next to the value — “Last updated 09:40, refresh failed” — not only with a greyed-out style. Screen reader users otherwise hear an outdated number as if it were current.
Should KPI cards announce when their values change?
Not individually. Let the dashboard announce one summary of meaningful changes after a refresh.
Related
Permalink to "Related"- Announcing dashboard refreshes — when KPI values change
- Numbers and units — writing values for speech
- Sonifying time series — hearing a sparkline’s shape