Skip to content

KPI tile

One number that matters, its label, and how it moved.

Beta · Content blocks · packages/css/src/components/kpi-tile.css · React on Base UI: @pepperui/kpi-tile

.pui-kpi-tile · tone --blue --red --yellow (neutral default) · __top __value at the top, __bottom (__label __detail) at the bottom, the row’s height between them; .pui-kpi-row lays a row. One tile may be negative. Only a STATUS delta is rung — --green up, --red down, a 2 px ring in slot white. A badge wearing the card’s own tint is a label, not a status: the grey flat below and every tag in the row under it are flush.

KPI tile — every size and every stateOpen in the workshop

Sizes across, states down.

  • Tone blue / red / yellow / neutral — a variant axis on kpi/tone/*/bg, not the Accent mode, because one row shows every team
  • The NEUTRAL tone follows the ground: kpi/tone/neutral/bg is surface/sunken — neutral/100 on a white page, neutral/200 on a surface/card product screen, one step darker than what it sits on. Set Ground = card on the frame that paints the product ground; the three tints do not move
  • Value (title/large — 36, 32 on mobile), Label (body/small — 16 / 14; 18 Medium on slides is the same role), Has delta with an exposed Badge (green or red by sign, small; swap its Tone) — the DELTA, a signed number and nothing else; Has tag with a second exposed Badge for a category (a team, an industry, a product — tone and Icon swapped, off by default), Has detail — a white slot with an Icon swap and a Detail line (body/medium)
  • 260 wide by default (four across a 1200 page at 32 gaps); FILL it in the row; heights hug — put the tiles in one auto-layout row so they match. Below about 240 wide the value wraps beside its delta: in a narrow column (the 896 beside the rail) go three across, never four
  • ONLY A STATUS DELTA IS RUNG — green because the number went up, red because it went down: a 2 px ring in kpi/delta/ring, stroke OUTSIDE, and nothing else in the library carries one. A badge in that corner wearing THE TILE’S OWN TINT — grey on the neutral tile, blue on the blue one — is a LABEL, not a status, and stays flush; so is the TAG, whatever its tone, because a red tag on a red tile is a team and not a fall. The reason a status needs it: the delta is the one thing here that says good or bad, and it says it in a tint-pair sitting on another tint, where the two edges run together; the ring cuts it out the way the white slot does lower down. A delta that is neither green nor red is not a delta — put it in Has tag. One or the other in that corner, not both
  • A glyph inside either badge takes THAT BADGE’S ink — tint/{tone}/fg, never text/default. In CSS it is currentColor and free; in Figma a swapped icon keeps its own stroke, so recolour it
  • Slots follow the inversion rule: the tinted tile carries a white slot
  • TWO BLOCKS pushed apart: Top (the value with its delta) at the top, Bottom (the label over the detail slot, 4 apart) at the bottom — SPACE_BETWEEN with 20 as the floor (Bottom’s top padding, space/20); a tile that FILLs a row’s height opens the room between them. Never a stack that runs top-down and leaves the bottom empty. In the detail slot the 24 glyph sits on the text’s top line, as in Icon list — never centred.
  • At least one tile in a row should be allowed to be negative — four green pills read as a brochure, not a product
  • Width narrow — below 200 px of its own width (four across on a tablet, two on a phone — the site’s 167): the Top row stacks, the value over its tag at space/4; the Value steps to title/medium; the tile sits 12 above and below, 20 at the sides. In code the tile answers its own width by a container query — no class; it takes its width from its row, never from its content (request 42)

Use when a dashboard opens with its headline numbers; a case study or a deck states a result in one figure; a stat needs a period-over-period movement next to it.

Do not use when the number needs a chart to mean anything (a Card with the chart in its slot); there are more than six of them (a Table); the tile would hold a paragraph (Card); it is a status, not a metric (Badge, Alert).

Relates to Card, Badge, Insight block, Chart recipes, Table.

Anti-patterns a ring on a category tag, or a delta with none · a delta and a tag in the same corner · a glyph in a different ink from the label beside it · four green deltas in one row · the same tint on every tile · a delta with no sign · a tile as a button · a value in a body size · the Accent mode used to colour a row of tiles (they must differ within the row).

Keywords: kpi, tile, metric, stat, number, delta, headline figure, dashboard, card metric

Figma — page KPI tile: KPI tile (8 variants)

The class, the parts, and nothing that is not in the package. The same block opens kpi-tile.css.

<div class="pui-kpi-tile pui-kpi-tile--blue">
<div class="pui-kpi-tile__top"><span class="pui-kpi-tile__value">$150K+</span><span class="pui-badge pui-badge--green pui-badge--small">+12 %</span></div>
<div class="pui-kpi-tile__bottom"><span class="pui-kpi-tile__label">Monthly revenue growth</span>
<div class="pui-kpi-tile__detail"><span class="pui-icon pui-icon--24">…</span>$ 8 969 916</div></div></div>
What it is Note
Narrow under 200 px of its OWN width (four across on a tablet, two on a phone — the site’s 167) the tile stacks the value over its tag at space/4, steps the value to title/medium and sits 12 / 20 inside (request 42). The tile is a size container for it: it takes its width from its row and never from its content —
Tone (none) neutral · --blue · --red · --yellow. The neutral tone FOLLOWS THE GROUND (ground.css): it is surface/sunken — neutral/100 on a white page, neutral/200 on the product ground; the three tints do not move —
Parts __tag — a category in the top corner instead of a delta; never rung, whatever its tone —

Rule — only a STATUS delta is rung — --green up, --red down. A badge wearing the card’s own tint (grey on grey, blue on blue) is a label, not a status, and stays flush

Every number here is measured on the rendered component: contrast (npm run contrast:render), the focus ring (npm run focus) and the keyboard (npm run keyboard). The floors are principle 12’s.

ink on ground reads floor the matrix calls it
#ec6663 on #f9d3d2 2.29 4.5 Red badge / chip (exempt) axe
#6a66e4 on #d4d3f7 3.12 4.5 Blue badge / chip (exempt) axe
#1a6b36 on #d4f2de 5.48 4.5 Success badge (promised) ok
#634e0f on #fad567 5.62 4.5 Yellow badge / chip (promised) ok
#544d4d on #dad9d9 5.85 4.5 text.muted on ground.card.sunken (promised) ok
#0b0202 on #e9e9fb 17.1 4.5 KPI value, blue tile (promised) ok
#0b0202 on #fce9e8 17.54 4.5 KPI value, red tile (promised) ok
#0b0202 on #f2f2f2 18.32 4.5 KPI value, neutral tile (promised) ok
#0b0202 on #fef9e9 19.47 4.5 KPI value, yellow tile (promised) ok
#0b0202 on #ffffff 20.51 4.5 text.default on surface.page (promised) ok

10 distinct pair(s) rendered, 2 that axe calls a violation — each one carries its reason in storybook/test/axe-allowlist.json.

Nothing in this component takes focus.

Nothing in this component claims an ARIA role with a keyboard contract; its controls are ordinary ones and the browser already gives them their keys.

Tab order across the whole library is document order over 344 stops, and Shift+Tab is its exact reverse — no keyboard trap anywhere (WCAG 2.1.2).

Pepper UI v1.5.0

The Mobupps design system. 386 tokens, 88 components, 65 pages in the workshop — every page of this site generated from the repository, so nothing here is typed twice.