Stat
A single figure: a label, a value and a change. Use it alone, in
a card, or in a row with .stat-group.
A label, a value and an optional change
The first span is the label and
strong is the value. An optional
span[data-trend] shows how it changed.
<div class="stat" style="max-width: 14rem">
<span>New orders</span>
<strong>128</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>12.5%</span>
</div>
One stat in a card
<div class="card" style="max-width: 20rem">
<div class="stat">
<span>Net revenue</span>
<strong>$1,377.43</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>4.2%</span>
</div>
</div>
A link to the records and an info button on the label
The whole stat can be an a, and the value turns the
accent color. For an explanation, a button in the
label opens a .popover (place the popover outside the
link).
Total sales from all orders before returns.
<div class="card" style="max-width: 20rem">
<div class="stat">
<span>Total sales<button type="button" popovertarget="stat-info" aria-label="About total sales"><i class="icon icon-info" aria-hidden="true"></i></button></span>
<strong><a href="#">$4,148.64</a></strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>40.1%</span>
</div>
<div class="popover" id="stat-info" popover>
<p>Total sales from all orders before returns.</p>
</div>
</div>
Trend colors: data-trend up is green, down is red, empty is gray
<div class="card">
<div class="stat-group">
<div class="stat">
<span>Rising</span>
<strong>128</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>12.5%</span>
</div>
<div class="stat">
<span>Falling</span>
<strong>96</strong>
<span data-trend="down"><i class="icon icon-arrow-down" aria-hidden="true"></i>25.0%</span>
</div>
<div class="stat">
<span>Unchanged</span>
<strong>40</strong>
<span data-trend>0.0%</span>
</div>
<div class="stat">
<span>No comparison</span>
<strong>7</strong>
</div>
</div>
</div>
data-reverse: when a rise is bad news
Add data-reverse to a stat to swap the colors: up is
red, down is green.
<div class="card" style="max-width: 28rem">
<div class="stat-group">
<div class="stat" data-reverse>
<span>Returns</span>
<strong>14</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>40.0%</span>
</div>
<div class="stat" data-reverse>
<span>Time to fulfill</span>
<strong>0.9 days</strong>
<span data-trend="down"><i class="icon icon-arrow-down" aria-hidden="true"></i>18.0%</span>
</div>
</div>
</div>
Loading: aria-busy="true" shows a spinner in place of the value
Put a .loader in the stat and toggle
aria-busy while fetching data. The value and the
change are hidden.
<div class="stack" data-gap="sm">
<div class="card" style="max-width: 28rem">
<div class="stat-group" id="stat-loading">
<div class="stat" aria-busy="true">
<span>New orders</span>
<strong>128</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>12.5%</span>
<span class="loader" role="status" aria-label="Loading"></span>
</div>
<div class="stat" aria-busy="true">
<span>Items ordered</span>
<strong>342</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>8.1%</span>
<span class="loader" role="status" aria-label="Loading"></span>
</div>
</div>
</div>
<div>
<button type="button" class="btn" data-size="sm" id="stat-busy-btn">Toggle loading</button>
</div>
</div>
<script>
const busyBtn = document.getElementById("stat-busy-btn");
busyBtn.addEventListener("click", () => {
const group = document.getElementById("stat-loading");
for (const s of group.children) {
const busy = s.getAttribute("aria-busy") === "true";
s.setAttribute("aria-busy", String(!busy));
}
});
</script>
A control beside the group stays outside it
The date range picker sits in a
data-direction="row" card next to the
.stat-group, not inside it, because the group clips
overflow.
<div class="card" data-direction="row">
<div>
<button type="button" class="btn" data-variant="ghost" data-size="sm" popovertarget="stat-period">
30 days
<i class="icon icon-chevron-down" aria-hidden="true"></i>
</button>
<div class="dropdown" id="stat-period" popover>
<button type="button" popovertarget="stat-period" popovertargetaction="hide">Today</button>
<button type="button" popovertarget="stat-period" popovertargetaction="hide">7 days</button>
<button type="button" popovertarget="stat-period" popovertargetaction="hide">30 days</button>
<button type="button" popovertarget="stat-period" popovertargetaction="hide">90 days</button>
</div>
</div>
<div class="stat-group">
<div class="stat">
<span>New orders</span>
<strong>128</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>12.5%</span>
</div>
<div class="stat">
<span>Items ordered</span>
<strong>342</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>8.1%</span>
</div>
<div class="stat" data-reverse>
<span>Returns</span>
<strong>6</strong>
<span data-trend="down"><i class="icon icon-arrow-down" aria-hidden="true"></i>25.0%</span>
</div>
<div class="stat">
<span>Fulfilled</span>
<strong>117</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>9.3%</span>
</div>
<div class="stat">
<span>Time to fulfill</span>
<strong>1.4 days</strong>
</div>
<div class="stat">
<span>Delivered</span>
<strong>102</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>3.0%</span>
</div>
</div>
</div>
Linked small stats beside a page title
data-size="sm" makes a small centered box. One here
is data-variant="danger" and another is still
loading.
<header class="page-header">
<h1>Orders</h1>
<div>
<a href="#" class="stat" data-size="sm" data-variant="danger">
<span>Rejected</span>
<strong>24</strong>
</a>
<a href="#" class="stat" data-size="sm">
<span>On hold</span>
<strong>2</strong>
</a>
<a href="#" class="stat" data-size="sm">
<span>Unfulfilled</span>
<strong>6</strong>
</a>
<a href="#" class="stat" data-size="sm" aria-busy="true">
<span>Processing</span>
<strong>0</strong>
<span class="loader" role="status" aria-label="Loading"></span>
</a>
<a href="#" class="btn" data-variant="primary">Create order</a>
</div>
</header>
Stats that pick what a chart shows
Each stat is a button, and the selected one has
aria-pressed="true". data-even on the
group makes every cell the same width.
<div class="card">
<div class="stat-group" data-even id="stat-pick">
<button type="button" class="stat" aria-pressed="false">
<span>New sessions</span>
<strong>4,812</strong>
<span data-trend="up"><i class="icon icon-arrow-up" aria-hidden="true"></i>12.4%</span>
</button>
<button type="button" class="stat" aria-pressed="false">
<span>Orders</span>
<strong>238</strong>
<span data-trend="down"><i class="icon icon-arrow-down" aria-hidden="true"></i>3.2%</span>
</button>
<button type="button" class="stat" aria-pressed="true">
<span>Sales</span>
<strong>$6,115</strong>
<span data-trend="down"><i class="icon icon-arrow-down" aria-hidden="true"></i>5.7%</span>
</button>
</div>
</div>
<script>
document.getElementById("stat-pick").addEventListener("click", (e) => {
const picked = e.target.closest(".stat");
if (!picked) return;
for (const s of e.currentTarget.children) {
s.setAttribute("aria-pressed", String(s === picked));
}
});
</script>
Legacy multi-cell box
dl.stat still works: each div has a
dt label and a dd value. For new work,
use .card > .stat-group instead.
- Revenue
- $3,668
- +12.4% from last month
- Refunds
- 18
- +2 items
<dl class="stat">
<div>
<dt>Revenue</dt>
<dd>$3,668</dd>
<dd data-variant="ok">+12.4% from last month</dd>
</div>
<div>
<dt>Refunds</dt>
<dd>18</dd>
<dd data-variant="danger">+2 items</dd>
</div>
</dl>
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.stat |
div, a, button
|
A single figure; a when the whole cell is a
link (accent-colored value), button when it
picks what to show
|
> span:first-child |
12px gray label, cut off with … when it does not fit; a
trailing icon button can open an
explanation
|
|
> strong |
24px value with tabular numbers | |
> span[data-trend] |
up, down; empty = gray |
The change, on the right of the value's row. up = ok color, down = danger color; add a leading icon |
> .loader |
Spinner shown in place of the value when
aria-busy="true" (hidden otherwise)
|
|
aria-busy |
true; omit to show the value (default)
|
Hides the value and the change, shows the
.loader
|
aria-pressed |
true, false |
On button.stat: the selected one gets a
darker gray fill
|
data-reverse |
(boolean) | Swaps the colors: up is danger, down is ok. For figures where a rise is bad |
data-variant |
danger; omit for normal (default) |
Red label and value, for counts that signal a problem |
data-size |
sm; omit for normal (default) |
Small bordered box, centered, for use beside a page title |
.stat-group |
div |
A row of stats separated by lines. Wraps on narrow
screens. Runs edge to edge inside a .card
|
.stat-group[data-even] |
(boolean) | Every cell gets the same width, but never narrower than its text |
dl.stat |
div > dt + dd + dd |
Legacy: several cells in one box;
dd[data-variant] = ok,
danger
|