One headline figure on a card, with a label, a change line in words, a caption and a trend.
Anatomy
A tile shows an overline label in slate capitals, the value as a small numeral, a change line with an arrow and your sentence, and a caption for the period. Only the label and the value are required. The tile uses the Card's surface, and its padding follows density.
Example
Example
Spending
$1,284.50
Up 12% from last month
Across all accounts
import { StatTile } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function StatTileBasic(): ReactElement {
return (
<div className="tile-row">
<StatTile
label="Spending"
value="$1,284.50"
change={{ direction: 'up', label: 'Up 12% from last month' }}
caption="Across all accounts"
/>
</div>
);
}The change line
The line is slate by default. Set tone to gain only for money that arrived, and to warning only for a state that needs a look. Direction never sets the color: "Down 12%" is good for spending and bad for income. The sentence carries the meaning, and the arrow repeats it.
Example
Example
Spending
$1,284.50
Up 12% from last month
Across all accounts
Income
$5,420.00
Up $420 from last month
Paid into checking
Savings
$12,000.00
No change in 3 weeks
Emergency fund
import { StatTile } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function StatTileTones(): ReactElement {
return (
<div className="tile-row">
{/* Neutral: more spending is not good or bad on its own, so the sentence says it and the color does not. */}
<StatTile
label="Spending"
value="$1,284.50"
change={{ direction: 'up', label: 'Up 12% from last month' }}
caption="Across all accounts"
/>
{/* Gain: money that arrived. */}
<StatTile
label="Income"
value="$5,420.00"
change={{ direction: 'up', label: 'Up $420 from last month', tone: 'gain' }}
caption="Paid into checking"
/>
{/* Warning: a state that needs a look. */}
<StatTile
label="Savings"
value="$12,000.00"
change={{ direction: 'flat', label: 'No change in 3 weeks', tone: 'warning' }}
caption="Emergency fund"
/>
</div>
);
}A change against a period
To say how a figure moved since the last period, write the change as the sentence you would say, with the period in it, and pass it as the change line:
<StatTile
label="Spent"
value="$4,220.00"
caption="October"
change={{ direction: 'up', label: 'Up $120 vs September' }}
/>It is a line and not a chip. A filled pill draws a delta as a badge, which reads as a state, and its fill would need a color; up and down mean nothing by themselves, since more spending and more income point the same way. The line keeps the tile's weight on its figure, reads as one sentence with the arrow repeating it, and takes a tone only for a meaning. The Insights recipe draws one per tile while its page compares.
A trend
Pass a small drawing, up to 96 by 32, in trend. The Sparkline is made for it. It sits at the end of the value's row, and moves under the caption when the tile has less than 18rem of content. It is hidden from assistive technology unless it names itself, since the text already says the change.
Example
Example
Net worth
$48,210.00
Up 3% this quarter
All accounts, less what you owe
import { Sparkline } from '@snapshot-ui/charts';
import { StatTile } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
import { NET_WORTH } from './sparkline-data';
export default function StatTileTrend(): ReactElement {
return (
<div className="tile-row">
<StatTile
label="Net worth"
value="$48,210.00"
change={{ direction: 'up', label: 'Up 3% this quarter' }}
caption="All accounts, less what you owe"
trend={<Sparkline data={NET_WORTH} />}
/>
</div>
);
}A gauge as the trend
A 32px Gauge fits the trend's slot, for a share rather than a change over time: credit used of a limit, a goal saved. A gauge is a meter that names itself, so it is read in the tile; give it an aria-label that says what share it is, and its value text beside it.
Example
Example
Credit used
$1,150.00
Of $5,000 across your cards
import { Gauge, StatTile } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function GaugeTile(): ReactElement {
return (
<div className="tile-row">
<StatTile
label="Credit used"
value="$1,150.00"
caption="Of $5,000 across your cards"
// A 32px ring fits the trend's 96 by 32 slot, named since the tile's words do not say the share.
trend={
<Gauge
aria-label="Credit used, share of your limits"
size="sm"
value={23}
valueLabel="23%"
marks={[
{ value: 10, label: 'Best under 10%' },
{ value: 30, label: 'Keep it under 30%' },
]}
/>
}
/>
</div>
);
}As a link
href makes the whole tile one link, routed through RouterProvider as Routing sets up. Its name is everything it shows, in order. Put nothing focusable inside it: a link cannot hold a link or a button, and the console warns when one does.
Example
Example
import { StatTile } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function StatTileLink(): ReactElement {
return (
<div className="tile-row">
{/* The whole tile is one link, named by everything it shows. */}
<StatTile
href="#spending"
label="Spending"
value="$1,284.50"
change={{ direction: 'up', label: 'Up 12% from last month' }}
caption="Across all accounts"
/>
</div>
);
}In a row
A tile is a size container, so it takes its width from its column, never from its content, and has no width of its own. Lay tiles in a grid such as repeat(auto-fit, minmax(16rem, 1fr)), not in a row that sizes them by content. Cap the minimum at the row's width, as below, so a column narrower than 16rem holds one tile and nothing scrolls sideways. Placed straight in a flex row a tile is 16rem wide and shrinks from there, since it takes flex: 0 1 16rem; give it flex: 1 1 16rem to share the row's room. In a flex column that basis would be its height, so give a tile there flex: none. In an auto grid column, give it a width.
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
gap: var(--spacing-peer);
}Usage
Pass every figure as a formatted string. The library formats no number or date, so the server's HTML is final in any locale. A tile without href renders in a server component with no client JavaScript. A linked tile is a client part.
import { StatTile } from '@snapshot-ui/react';
<StatTile
label="Spending"
value="$1,284.50"
change={{ direction: 'up', label: 'Up 12% from last month' }}
caption="Across all accounts"
href="/spending"
/>;Props
A plain StatTile accepts every attribute of an HTML element. A linked one is React Aria's Link, which forwards only id, data-*, lang, dir, hidden, style, aria-label, aria-labelledby, aria-describedby, aria-details, aria-current and the event handlers. Others, such as title or role, apply only to the plain tile.
caption ReactNode | |
|---|---|
change StatTileChange | |
className string | |
href string | Makes the whole tile a link. |
label ReactNode | |
level 2 | 3 | 4 | The heading level of the label, when the tile should be in the page outline. Omitted, the label is a |
trend ReactNode | A Sparkline, or any small drawing. Hidden from assistive technology unless it names itself. |
value ReactNode | The figure, formatted. |
direction "down" | "flat" | "up" | The arrow, and the word the app writes. |
|---|---|
label string | The whole sentence, formatted: "Up 12% from last month". It carries the meaning; the arrow repeats it. |
tone "gain" | "neutral" | "warning" | Color only when the change means one of these. Default: 'neutral' |
Accessibility
The tile is read as its text, in order: the label, the value, the change and the caption. A linked tile is one link named by that text. The arrow is hidden, since the sentence says the change. level makes the label a heading, so the tile joins the page outline.
| Tab | Moves to a linked tile, one stop for the whole tile. |
|---|---|
| Enter | Follows the link. |
Design rules
What keeps stat tiles in the system.
Show one figure per tile.
Say the change in words, in a sentence.
Give a caption for the period the figure covers.
Never color a change by its direction.
Never put a button or a link inside a linked tile.
Never pass a figure for the library to format. Every figure is your string.