Minimal
A label and a value. The label truncates if it runs long; the value is the hero. Nothing else is drawn, so a bare metric stays bare.
<StatCard label="Active orgs" value="276" />Compact KPI tile with an inline sparkline and trend chip.
A calm KPI / stat tile: a neutral hero number, a faint tinted icon, and a sentiment-aware trend chip (green for good, red for bad, set per metric with goodDirection). Its optional pure-line sparkline is drawn as inline SVG, so the card pulls in no chart library. Renders a real keyboard-focusable button or anchor when interactive, a shimmer skeleton while loading, and ships its own light / dark tokens. It never fabricates data — omit the series and the chart simply does not draw.
Add the package to your project. The card ships its own CSS, draws its own sparkline, and pulls in no icon or chart library — there's nothing else to wire up.
pnpm add @roy-ui/uiimport { StatCard } from '@roy-ui/ui';
// or import just this component (its own 'use client' island):
import { StatCard } from '@roy-ui/ui/stat-card';At its smallest the card is a label and a value. Layer on a delta chip, a sparkline, an icon, or a link as the metric earns them. The previews below run dark to read against this page; the card is theme-aware either way.
A label and a value. The label truncates if it runs long; the value is the hero. Nothing else is drawn, so a bare metric stays bare.
<StatCard label="Active orgs" value="276" />Pass a real data series and the card draws a smooth monotone sparkline beneath the value, stroked in the accent color. Hover the card and the line redraws itself from the start; tune the pace with drawDuration (it runs at a relaxed 1200ms here). A signed delta becomes a chip whose arrow and color follow the change.
<StatCard
label="Revenue"
value="$125.4K"
sub="+$42.1K this month"
delta={12.4}
data={[56, 47, 42, 52, 66, 83, 98]}
icon={<RevenueIcon />}
/>Color tracks sentiment, not raw direction. Revenue is up, so its chip is green. Error rate is down — but a falling error rate is the good outcome, so with goodDirection="down" that drop reads green too. The component knows which way is the right way for each metric.
// up is good by default — a rising number reads green
<StatCard label="Revenue" value="$125.4K" delta={12.4} />
// down is good here — a falling number reads green, not red
<StatCard
label="Error rate"
value="0.42%"
sub="Down from 0.71%"
delta={-12.5}
goodDirection="down"
/>Set compact for a denser card, then drop a handful into an auto-fitting grid — this is the dashboard shape. Each card carries its own accent, and a few of these data series prove the no-chart cases below sit comfortably alongside the charted ones.
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: 8,
}}>
{statCardSamples.map((s) => (
<StatCard key={s.label} compact {...s} />
))}
</div>Pass onClick and the whole card becomes a real <button> — keyboard-focusable, with a focus ring and an aria-label composed from the label and value. Pass href instead and it renders as an <a>. With neither it stays a plain <div>, so it never invites a click it can't honor.
<StatCard
label="Active users"
value="12,840"
sub="+1,930 this week"
delta={6.8}
data={[10180, 10520, 17640, 13080, 12200, 12460, 12840]}
icon={<PulseIcon />}
onClick={() => router.push('/users')}
/>Set loading and the value and sparkline become a shimmer skeleton while the real numbers are in flight. The label and icon stay put, so the card holds its shape and doesn't jump when the data lands.
<StatCard
label="p95 latency"
value="184ms"
data={[241, 233, 218, 205, 199, 190, 184]}
loading
/>Never fabricate a series. When there's no honest data to plot, omit the data prop — the card renders cleanly with just the value, no invented line. A single point or an all-NaN array draws nothing either.
<StatCard
label="Avg. score"
value="7.6 / 10"
sub="Last 30 days"
/>By default the card follows the system (theme="auto"), so it inherits your site's mode. It ships its own light and dark tokens — no Tailwind, no theme provider. Force either side with theme="light" or theme="dark".
// default — follows the system / your site theme
<StatCard {...kpi} />
// force one side
<StatCard theme="light" {...kpi} />
<StatCard theme="dark" {...kpi} />The card exposes its surface, ink, and accents as CSS variables. Override them inline or in a stylesheet — it already ships a prefers-color-scheme: dark theme, and theme="dark" forces it.
.royui-statcard {
--royui-statcard-bg: #ffffff; /* card surface */
--royui-statcard-fg: #171717; /* the hero value (neutral ink) */
--royui-statcard-label: #666666; /* label */
--royui-statcard-faint: #8f8f8f; /* sub + flat chip */
--royui-statcard-line: #ededed; /* border */
--royui-statcard-accent: #b8880a; /* default icon + sparkline */
--royui-statcard-good: #15803d; /* good-direction chip */
--royui-statcard-bad: #c4332b; /* bad-direction chip */
--royui-statcard-radius: 12px;
}label and value are the only required props. Any other native HTML attribute spreads onto the root element — whether that's a div, a button, or an anchor.
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Required. The KPI title. Truncates if it runs long. |
value | ReactNode | — | Required. The hero metric. Empty (null / undefined / "" / NaN) falls back to emptyValue. |
sub | ReactNode | — | Muted secondary line beneath the value. |
trend | 'up' | 'down' | 'flat' | — | Explicit chip direction — sets the arrow and sentiment color. |
delta | number | — | Signed percentage. The chip shows its absolute value plus "%". |
goodDirection | 'up' | 'down' | 'up' | Which direction reads green. Set "down" so falling churn or latency reads positive. |
data | number[] | — | Sparkline series, real data only. Draws only with two or more finite points. |
color | string | gold | Accent for the icon and sparkline stroke. Any CSS color. |
icon | ReactNode | — | Leading glyph, tinted with color. Bring your own. |
href | string | — | Renders the whole card as an <a> link. |
onClick | () => void | — | Renders the whole card as a keyboard-accessible <button>. |
loading | boolean | false | Shimmer skeleton for the value and sparkline. |
emptyValue | ReactNode | '—' | Placeholder shown when value is empty. |
compact | boolean | false | Denser variant for dashboard grids. |
theme | 'light' | 'dark' | 'auto' | 'auto' | Follow the system (default), or force the built-in light or dark tokens. |
drawDuration | number | 1200 | Milliseconds for the hover draw-in of the sparkline. Lower is snappier; 0 disables it. |
className | string | — | Extra classes. Other native HTML attributes spread onto the root, too. |