Quire UI
Data display6

Stat card

One figure with its change and a sparkline you can read point by point, by pointer, touch or keyboard. No chart library.

Revenue
$50,335
Up 15.9%vs September
Signups
1,779
Up 8.1%vs September
Response time
126 ms
Down 14.2%vs September

Installation

npx shadcn@latest add @quireui-pro/stat-card

Part of Pro. Add the Pro registry with your token once, as shown in Installation, and the CLI brings in the free parts it builds on as well.

Usage

<StatCard
  label="Revenue"
  value={50335}
  format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
  delta={15.9}
  period="vs September"
/>

A number goes through format, which takes the options of Intl.NumberFormat or your own function. A string, like a plan name, shows as it is.

Long numbers and definitions

notation: "compact" turns 1,204,000 into $1.2M, in the headline and in the sparkline alike. action holds anything at the end of the label row, like a tooltip saying how the figure is worked out, so nobody has to guess what is counted.

Annual run rate
$1.2M
Up 9.3%vs last year
<StatCard
  label="Annual run rate"
  value={1204000}
  format={{ style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1 }}
  action={
    <Tooltip>
      <TooltipTrigger aria-label="About annual run rate">
        <InfoIcon />
      </TooltipTrigger>
      <TooltipContent>This month's recurring revenue × 12</TooltipContent>
    </Tooltip>
  }
/>

The change

delta is the signed change against period. Up is green and down is red, and an arrow says the same for anyone who cannot tell the colours apart. For figures where less is better, like response time or churn, lowerIsBetter swaps the colours and keeps the arrow pointing the way the number went.

A percentage misleads on small counts: going from 3 to 4 reads as "33%". deltaUnit="value" shows the change itself.

Outstanding
$18,240
Down 3.2%vs last month
Overdue
4
Up 1since Monday

Reading a point

series draws the sparkline. Move along it and the headline becomes that point, with its label where the period was. Nothing else on the card moves, since the delta has its own row. Let go and the total comes back.

<StatCard
  label="Revenue"
  value={50335}
  format={money}
  series={[
    { label: "Oct 1", value: 1320 },
    { label: "Oct 2", value: 1410 },
    // ...
  ]}
  previous={[1180, 1255 /* ... */]}
/>

previous is the period before, point for point. It draws a dashed line behind the series, and gives each point its own change. Without it the delta stays out of the way while you read a point, because a single point has nothing to be compared with.

Bars

chart="bars" draws one bar per point, for counts like signups or orders per day. Bars stand on zero, so their heights can be compared; a line zooms in on its own range, which suits a level that moves, like response time. Reading a point works the same, and the bar being read goes solid.

A period still running

Today's figure always looks like a crash at the end of the line, because the day is not over. partial says the last point is still being counted: the line turns dashed for its last step, or the bar fades, and the point reads "Oct 30, so far". It has no change of its own, since half a day against a whole one says nothing.

Revenue
$48,710
this month
Signups
1,711
this month
<StatCard label="Revenue" value={48710} format={money} series={revenue} partial />

A row of cards

StatCardGroup wraps to fit its width and ties the sparklines together: looking at Oct 12 on one card shows Oct 12 on all of them, so you can see what Signups did on the day Revenue dipped. Points are matched by label, and a card without that point stays on its total.

<StatCardGroup aria-label="October">
  <StatCard label="Revenue" {...revenue} />
  <StatCard label="Signups" {...signups} />
  <StatCard label="Response time" lowerIsBetter {...latency} />
</StatCardGroup>

Goals

goal adds a bar showing how far value is toward a target.

Quarterly revenue
$142,600
Up 6.8%vs last quarter
79% of $180,000 goal
x

link makes the whole card one target that leads to the full report. The sparkline stays a sparkline, so reading a point never navigates.

$50,335
Up 15.9%vs September
<StatCard label="Revenue" link={{ render: <Link href="/revenue" /> }} {...revenue} />

Loading, empty and failed

loading shows placeholders in the shape of the figures, so nothing moves when they arrive. Pass series={[]} while loading to hold the sparkline's space. value={null} says there is no data, which is different from a zero. error says the figures could not load, and onRetry offers Retry.

Revenue
Refunds
No data
Signups
Could not load

Accessibility

The direction of a change is read out in words, like "Up 15.9%, vs September", and never relies on colour. The sparkline is a slider: screen readers hear its name, "Revenue over time", then each point as you move, like "Oct 12: $1,745, Up 15.6%".

KeyAction
← / →, ↓ / ↑Previous or next point
PageDown / PageUpBack or forward a tenth of the series
Home / EndFirst or last point
EscBack to the total

Motion

None on purpose. The line and the headline follow the pointer and the keys without easing, and figures do not count up: a number that is still rolling is a number you cannot read yet.

Stat card or something else?

  • A plain amount in a table or a list: a Table with tabular numbers.
  • How much of a limit is used, like storage or seats: a Meter.

API reference

StatCard

One figure with its change and, when there is a series, a sparkline that reads out any point. Hovering, touching or arrowing along the line swaps the headline for that point.

Prop

Type

Style it with [data-slot="stat-card"].

StatCardGroup

A row of stat cards that wraps to fit. Their sparklines move together: looking at Oct 12 on one shows Oct 12 on all of them, matched by each point's label.

Also takes every <div> attribute. Style it with [data-slot="stat-card-group"].

On this page