Metric Card
A card that reads a title, a number, and a trend, charted as a line, bars over time, or a comparison across categories.
MetricCard pairs a headline number and trend with a chart of the data behind
it. Give the card a fixed width and drop a VerticalBarChart,
LineChart, or HorizontalBarChart
inside as its child.
Choosing a chart
Pick the chart by the question the reader is asking.
| Chart | The reader asks | Data |
|---|---|---|
| Line Chart | Is it going up or down? | One series over many time points, about 14 or more, read as a shape |
| Vertical Bar Chart | How did each period compare, and what made it up? | About 7 to 14 periods, each a separate total, stacked when split into parts |
| Horizontal Bar Chart | Which is biggest, and by how much? | Named categories with no time order, such as chains, apps, or tokens |
- Use bars for totals per period, such as sales per day or mints per week, and a line for a level that moves, such as price, TVL, or active users.
- If the order along the axis doesn't matter, don't draw a line. A line implies the points between values exist.
- Bars start at zero, so their lengths compare honestly. A line's scale covers the range of its data, which suits small changes in a large number.
- Stack bars only when the split matters. If only the total matters, use a line or give Vertical Bar Chart a single series.
- Past about 8 categories, show the top ones and group the rest as Other, or use a table.
Vertical Bar Chart
VerticalBarChart draws one bar per period. Each entry in series is a
label and its data points; points from different series line up by
timestamp, and a missing point counts as zero.
Mints
<MetricCard title="Mints" numValue={2310} trend="+4.1%" trendPositive>
<VerticalBarChart
period="last-week"
series={[
{
label: 'Mints',
data: [
{ timestamp: '2024-01-01T12:00:00.000Z', value: 300 },
{ timestamp: '2024-01-02T12:00:00.000Z', value: 320 },
// β¦one point per day
],
},
]}
/>
</MetricCard>period sets the date format in the tooltip and on the x axis, and how many
placeholder bars to draw while loading. Bars start at zero and the tallest
one fills the plot. Hovering a bar shows its date and value. suffix on the
card renders a unit alongside the number, baseline-aligned. See
Choosing a chart for when bars fit.
Stacked
Supply vs Borrow
<MetricCard title="Supply vs Borrow" numValue={450} suffix="ETH" trend="+5.2%" trendPositive>
<VerticalBarChart
period="last-week"
series={[
{ label: 'Supply', data: supplyPoints },
{ label: 'Borrow', data: borrowPoints },
]}
/>
</MetricCard>Pass two or more series and the bars stack, first series on top, with a 2px gap between parts. There is no prop to turn stacking on or off; the number of series decides it. The tooltip lists every part. A single series uses the strong blue; stacked series use light blue, then strong blue, then two more steps of the blue scale. Two or three parts read well; past that, the thin parts are hard to compare.
visibleSeries takes the indices of the series to draw, such as [1] to
show only the second. Hidden series drop out of the bars, the scale, and the
tooltip. Pair it with a control in the card's filter slot.
Migrating from Stacked Bar Chart
StackedBarChart is deprecated and will be removed in the next release.
Until then it renders through VerticalBarChart. Replace it like this:
// Before
<StackedBarChart
bars={buildSegmentBars(supplyPoints, borrowPoints)}
allBars={buildSegmentBars(supplyPoints, borrowPoints)}
period="last-week"
userFilter="first"
config={{ labels: ['Supply', 'Borrow'] }}
/>
// After
<VerticalBarChart
period="last-week"
series={[
{ label: 'Supply', data: supplyPoints },
{ label: 'Borrow', data: borrowPoints },
]}
visibleSeries={[0]}
/>Each series takes its ChartDataPoint array as data, so buildSegmentBars
is no longer needed. userFilter becomes visibleSeries: all is the
default, first is [0], and second is [1]. allBars has no
replacement.
Horizontal Bar Chart
HorizontalBarChart compares one value across named categories. Each
category gets a row: its name on the left and a bar whose length is its
value.
Volume by Chain
- Base: 1,200
- Ethereum: 800
- Arbitrum: 520
- Optimism: 430
<MetricCard title="Volume by Chain" numValue={2950} suffix="ETH">
<HorizontalBarChart
data={[
{ label: 'Base', value: 1200 },
{ label: 'Ethereum', value: 800 },
{ label: 'Arbitrum', value: 520 },
{ label: 'Optimism', value: 430 },
]}
xAxis
/>
</MetricCard>Rows are drawn top to bottom in the order of data, so sort it first,
usually largest first. The longest bar fills the row and the rest scale
against it. Labels take the width of the longest one, up to 128px, and
truncate past that. xAxis adds a value scale under the bars with dashed
gridlines, starting at zero. Hovering a row shows its label and value.
Pass formatValue to format that value, for example to add a unit; it is a
function, so set it from a client component.
The card grows to fit the rows instead of holding its fixed height, so keep the list short; about 3 to 8 categories read well. For values over time, see Choosing a chart.
Line Chart
LineChart is the trend chart for MetricCard β a single series over time,
scrubbable, with an optional gradient fill.
Total Volume
<MetricCard
title="Total Volume"
numValue={1234567}
trend="+12.3%"
trendPositive
tooltip="Total trading volume over the selected period"
>
<LineChart
dataPoints={[
{ timestamp: '2024-01-01T00:00:00.000Z', value: 420 },
{ timestamp: '2024-01-02T00:00:00.000Z', value: 510 },
{ timestamp: '2024-01-03T00:00:00.000Z', value: 480 },
{ timestamp: '2024-01-04T00:00:00.000Z', value: 620 },
{ timestamp: '2024-01-05T00:00:00.000Z', value: 590 },
{ timestamp: '2024-01-06T00:00:00.000Z', value: 700 },
{ timestamp: '2024-01-07T00:00:00.000Z', value: 660 },
]}
period="last-week"
gradient
/>
</MetricCard>trend is a preformatted string like +12.3%; trendPositive sets its colour
and arrow direction. A trend of exactly 0% is hidden. Use tooltip for a
one-line explanation of what the number counts. See
Choosing a chart for when a line fits.
Axes and legend
Supply vs Borrow
- Supply
- Borrow
<MetricCard
title="Supply vs Borrow"
numValue={510}
suffix="ETH"
trend="+5.2%"
legend={<ChartLegend labels={['Supply', 'Borrow']} shape="square" />}
>
<VerticalBarChart
period="last-week"
series={[
{ label: 'Supply', data: supplyPoints },
{ label: 'Borrow', data: borrowPoints },
]}
xAxis
yAxis
/>
</MetricCard>Supply vs Borrow
- Supply
- Borrow
<MetricCard
title="Supply vs Borrow"
numValue={510}
suffix="ETH"
trend="+5.2%"
legend={<ChartLegend labels={['Supply', 'Borrow']} shape="line" />}
>
<LineChart
segmentBars={buildSegmentBars(supplyPoints, borrowPoints)}
segmentConfig={{ labels: ['Supply', 'Borrow'] }}
period="last-week"
xAxis
yAxis
/>
</MetricCard>Axes and the legend are off by default; turn on only the ones the reader
needs. xAxis labels the dates under the chart: every day for a week, every
hour for today, and about 5 evenly spaced dates for longer ranges. yAxis
adds a value scale on the right with dashed gridlines, in compact numbers
such as 1.2k. The bar chart's scale starts at zero; a single-series line
chart's scale covers the range of its data. When either axis is on, the card
grows from 161px to 200px to fit the labels.
Pass a ChartLegend to the card's legend slot to name each series. It sits
in the top-right corner, before any linkLabel button or filter. Use
shape="square" with bars and shape="line" with lines, and list the labels
in the same order as the chart's series so the colours match. A single
series needs no legend; the card title names it.
Header
Link
Active Users
<MetricCard title="Active Users" numValue={8421} linkLabel="View All" onLinkPress={openUsers} />linkLabel adds a small bare button with an arrow at the end of the title row;
onLinkPress runs when it is pressed. Use it to open the full report behind the
number.
Filter
Fees
<MetricCard
title="Fees"
numValue={312}
suffix="ETH"
filter={
<Segments aria-label="Period" defaultValue="7d">
<Segment value="7d">7D</Segment>
<Segment value="30d">30D</Segment>
</Segments>
}
/>filter renders whatever control you pass at the end of the title row, such as
a period switch. The card does not filter anything itself β update its value and
chart when the control changes.
Preformatted value
Treasury
<MetricCard title="Treasury" value="$1.2M" trend="-0.4%" trendPositive={false} />Pass value instead of numValue when the number is already formatted, such as
an abbreviated currency. numValue is formatted with the reader's locale and
can be animated with renderAnimatedValue; value renders as given. Both use
tabular numerals, as do the trend and the chart tooltips, so digits do not
shift as the value changes.
Loading
Supply vs Borrow
<MetricCard title="Supply vs Borrow" loading>
<VerticalBarChart loading period="last-week" series={[]} />
</MetricCard>Active Users
<MetricCard title="Active Users" loading>
<LineChart loading period="last-week" />
</MetricCard>Volume by Chain
<MetricCard title="Volume by Chain" loading>
<HorizontalBarChart loading data={[]} loadingRows={4} />
</MetricCard>Set loading on the card and on the chart together β the card shows a
skeleton in place of the value while the chart shows its own placeholder, so
the layout does not shift when data arrives. The Vertical Bar Chart draws one
placeholder bar per slot in period, so the bar count matches what the data
will fill in. The Horizontal Bar Chart draws loadingRows placeholder rows, 5
by default; match it to the number of categories you expect.
When to use
Use MetricCard for a headline number, its trend, and the data behind it
charted as a line, bars over time, or a comparison across categories.
Use something else when:
- The number needs no chart or trend. Set it as text with tabular numerals; see Typography.
- You are reporting a state, not a quantity. Use Status.
- The comparison has more than about 8 categories, or readers need exact values side by side. Use a table.
Usage
Do
Constrain the card's width with a wrapper such as w-80, or lay several out
on a responsive grid. The chart fills the width it is given.
Don't
Bake an arrow or a colour into trend. Pass the formatted change, like
+12.3%, and let trendPositive decide the colour and arrow direction.
Do
Sort Horizontal Bar Chart data before passing it, usually largest first, so the ranking reads top to bottom.
Don't
Draw a Line Chart across categories such as chains or apps. Use a Horizontal Bar Chart; a line implies an order the data doesn't have.
Props
| Prop | Type | Default |
|---|---|---|
clipContent | Typeboolean | Defaultfalse |
filter | TypeReactNode | Defaultβ |
legendA ChartLegend, shown in the top-right corner beside any `filter`. | TypeReactNode | Defaultβ |
linkLabel | Typestring | Defaultβ |
loading | Typeboolean | Defaultfalse |
numValue | Typenumber | Defaultβ |
onLinkPress | Type(() => void) | Defaultβ |
renderAnimatedValue | Type((value: number) => ReactNode) | Defaultβ |
suffix | Typestring | Defaultβ |
title | Typestring | Defaultβ |
tooltip | Typestring | Defaultβ |
trend | Typestring | Defaultβ |
trendPositive | Typeboolean | Defaulttrue |
value | TypeReactNode | Defaultβ |
Vertical Bar Chart
| Prop | Type | Default |
|---|---|---|
periodrequiredTime range the bars cover. Sets the date format and the number of placeholder bars. | Type"today" | "last-week" | "last-month" | "last-90-days" | Defaultβ |
seriesrequiredOne series draws plain bars; two or more stack, first series on top. Points line up by `timestamp`. | TypeVerticalBarSeries[] | Defaultβ |
loadingShow one placeholder bar per slot in `period` while data loads. | Typeboolean | Defaultfalse |
visibleSeriesIndices of the series to draw. Hidden series drop out of the bars, the scale, and the tooltip. Defaults to all. | Typenumber[] | Defaultβ |
xAxisLabel each bar's date under the chart. Long ranges show about 5 evenly spaced dates. | Typeboolean | Defaultfalse |
yAxisShow a value scale on the right with dashed gridlines. | Typeboolean | Defaultfalse |
Horizontal Bar Chart
| Prop | Type | Default |
|---|---|---|
datarequiredOne bar per category, drawn top to bottom in the order given. | TypeHorizontalBarData[] | Defaultβ |
formatValueFormats each value in the tooltip and for assistive technology. | Type((value: number) => string) | Defaultβ |
loadingShow placeholder rows while data loads. | Typeboolean | Defaultfalse |
loadingRowsNumber of placeholder rows while `loading` and `data` is empty. | Typenumber | Default5 |
xAxisShow a value scale under the bars with dashed gridlines. | Typeboolean | Defaultfalse |
Line Chart
| Prop | Type | Default |
|---|---|---|
className | Typestring | Defaultβ |
dataPoints | TypeChartDataPoint[] | Defaultβ |
fadeLeft | Typestring | Defaultβ |
formatValue | Type((raw: string | number) => string) | Defaultβ |
gradient | Typeboolean | Defaultfalse |
loading | Typeboolean | Defaultfalse |
period | Type"today" | "last-week" | "last-month" | "last-90-days" | Defaultlast-week |
segmentBars | TypeSegmentBarData[] | Defaultβ |
segmentConfig | TypeSegmentConfig | Defaultβ |
visibleSeriesIndices | Typenumber[] | Defaultβ |
xAxisLabel dates under the chart. Long ranges show about 5 evenly spaced dates. | Typeboolean | Defaultfalse |
yAxisShow a value scale on the right with dashed gridlines. | Typeboolean | Defaultfalse |
Chart Legend
| Prop | Type | Default |
|---|---|---|
labelsrequiredSeries names, in the same order as the chart's series. | Typereadonly string[] | Defaultβ |
colorsSwatch colour per series. Defaults to the MetricCard chart colours. | Typereadonly string[] | Defaultβ |
shape | Type"line" | "square" | null | Defaultsquare |
Accessibility
MetricCard renders a plain container, so the title and value read as ordinary
text. The optional tooltip uses React Aria's Tooltip on an icon button that
carries its own accessible name, and it opens on keyboard focus as well as
hover.
The charts are visual summaries: their data points are not exposed to assistive technology. Make sure the headline value and trend carry the takeaway, and offer the underlying data elsewhere, such as a table, when readers need the individual points.
HorizontalBarChart is the exception: it renders a list, and each item reads
its category and value, such as "Base: 1,200 ETH". The bars, tooltip, and
scale are hidden.
Axis labels are hidden from assistive technology along with the rest of the
chart. ChartLegend renders a list, so its series names read
as ordinary text.
When the reader prefers reduced motion, the bar charts' tooltips and bar size changes happen at once.
The info button around the title's tooltip icon has a 24px hit area, though
the icon itself is 14px.