Skip to content
omaris

Data Display

Stat Card

The KPI tile every dashboard opens with: a label, one number, and how that number moved.

import { StatCard } from 'omaris'
Learn

Examples

Basic

A label, one number and how it moved. delta is a fraction — 0.124 is +12.4% — and its sign picks the arrow and the colour.

Revenue $48,210
increased by 12.4% vs last week
<script lang="ts">	import { StatCard } from 'omaris';</script>​<StatCard	class="w-72 max-w-full"	label="Revenue"	value={48210}	format={{ style: 'currency', currency: 'USD', maximumFractionDigits: 0 }}	delta={0.124}	deltaLabel="vs last week"/>

Kpi row

The row a dashboard opens with: one column on a phone, two on a tablet, four on a desktop. A Sparkline from omaris/chart goes in the chart snippet, and invert is for a metric where down is the good news.

Revenue $48,210
increased by 12.4% vs last week
Orders 1,284
increased by 6.1% vs last week
Refunds 23
decreased by 18% vs last week
Avg. delivery 42 min
increased by 5% vs last week
<script lang="ts">	import { StatCard } from 'omaris';	import { Sparkline } from 'omaris/chart';​	type Kpi = {		label: string;		value: number | string;		format?: Intl.NumberFormatOptions;		delta: number;		invert?: boolean;		trend: number[];		color: string;	};​	const KPIS: Kpi[] = [		{			label: 'Revenue',			value: 48210,			format: { style: 'currency', currency: 'USD', maximumFractionDigits: 0 },			delta: 0.124,			trend: [32, 35, 31, 38, 42, 40, 45, 44, 49, 52],			color: 'var(--chart-1)'		},		{			label: 'Orders',			value: 1284,			delta: 0.061,			trend: [90, 96, 94, 101, 99, 108, 112, 110, 118, 121],			color: 'var(--chart-2)'		},		{			label: 'Refunds',			value: 23,			delta: -0.18,			invert: true,			trend: [34, 31, 33, 29, 30, 27, 26, 28, 24, 23],			color: 'var(--chart-3)'		},		{			label: 'Avg. delivery',			value: '42 min',			delta: 0.05,			invert: true,			trend: [38, 39, 37, 40, 41, 39, 42, 43, 41, 42],			color: 'var(--chart-4)'		}	];</script>​<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">	{#each KPIS as kpi (kpi.label)}		<StatCard			variant="elevated"			label={kpi.label}			value={kpi.value}			format={kpi.format}			delta={kpi.delta}			invert={kpi.invert}			deltaLabel="vs last week"		>			{#snippet chart()}<Sparkline data={kpi.trend} height={36} color={kpi.color} />{/snippet}		</StatCard>	{/each}</div>

States

loading keeps the label and swaps the rest for skeletons of the same size, so nothing jumps when the figure lands. animate counts up to it, and does nothing at all under reduced motion.

Active users
Conversion
<script lang="ts">	import { Button, StatCard } from 'omaris';​	let loading = $state(true);</script>​<div class="flex w-full max-w-2xl flex-col gap-4">	<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">		<StatCard {loading} animate label="Active users" value={8214} delta={0.041} deltaLabel="today">			{#snippet icon()}				...			{/snippet}		</StatCard>		<StatCard			{loading}			animate			variant="filled"			tone="tertiary"			label="Conversion"			value={0.0342}			format={{ style: 'percent', maximumFractionDigits: 2 }}			delta={-0.012}			deltaLabel="vs yesterday"		/>	</div>	<Button variant="tonal" class="self-start" onclick={() => (loading = !loading)}>		{loading ? 'Load figures' : 'Back to loading'}	</Button></div>

Interactive

Give it href or onclick and the whole tile is one target — state layer, ripple, and a press that gives under the finger.

<script lang="ts">	import { StatCard } from 'omaris';</script>​<div class="grid w-full max-w-2xl grid-cols-1 gap-4 sm:grid-cols-2">	<StatCard href="#" label="Open tickets" value={37} delta={-0.2} invert deltaLabel="since Monday">		{#snippet footer()}View the queue →{/snippet}	</StatCard>	<StatCard		href="#"		variant="elevated"		label="Next payout"		value={12480}		format={{ style: 'currency', currency: 'USD' }}		deltaLabel="Friday, 3 April"	>		{#snippet footer()}Payout schedule →{/snippet}	</StatCard></div>

Overridden

class on the tile wins over its own radius, classes reaches every part, and the caller's role, aria-label and dir stay. locale="ar-IQ" formats the Arabic way with Western digits, as every number here does.

الإيرادات ‏1,250,000 د.ع.‏
increased by 20‎%‎ مقارنة بالشهر الماضي
<script lang="ts">	import { StatCard } from 'omaris';</script>​<StatCard	class="w-72 max-w-full rounded-shape-2xl"	classes={{ value: 'text-primary', delta: 'rounded-shape-xs', icon: 'rounded-full' }}	role="group"	aria-label="الإيرادات هذا الشهر"	dir="rtl"	label="الإيرادات"	value={1250000}	format={{ style: 'currency', currency: 'IQD', maximumFractionDigits: 0 }}	locale="ar-IQ"	delta={0.2}	deltaLabel="مقارنة بالشهر الماضي">	{#snippet icon()}		...	{/snippet}</StatCard>

When to use it

Use it for

  • The KPI row at the top of a dashboard — revenue, orders, active users — in a grid that goes one, two, then four columns wide.
  • One number and how it moved: delta as a fraction (0.124 is +12.4%) with a deltaLabel that says against what — "vs last week".
  • A metric where down is the good news — churn, refunds, response time — with invert, so a fall is green and a rise is red.
  • A trend under the figure: a Sparkline from omaris/chart in the chart snippet.
  • A tile that opens the report behind it: href or onclick makes the whole tile one target.

Not for

  • A chart that is the point of the card → Charts in a Card; a stat card's chart is a sparkline under a number.
  • Several figures about one thing — an order's total, tax and delivery → Data List.
  • A count on an icon or a tab → Badge.
  • Progress towards a target — storage used, a quota → Progress or Gauge.

Do

  • Pass numbers, not strings, and describe them with format — currency, notation: 'compact', fraction digits. Digits stay Western in every locale.
  • Keep the label to a couple of words; it truncates rather than wrapping.
  • Use loading while the figure is on its way. The label stays and the tile keeps its height, so the row does not jump when the numbers land.
  • Pick one variant for the whole row — elevated on a plain page, filled inside another surface.

Don't

  • Pass a delta already multiplied by 100 — 12.4 reads as +1,240%. Use 0.124, or deltaFormat={{ style: 'decimal' }} for a delta that is a count.
  • Put a button or a link inside a tile that has href or onclick; the tile is already the target.
  • Turn on animate for a whole grid of tiles that refresh every few seconds; count up the one figure that matters.
  • Colour a tile's value by hand to say good or bad — that is what the delta chip and invert are for.

Quick reference

tone
  • primary (default)
  • secondary
  • tertiary
  • destructive
  • success
  • warning
  • info

API

StatCard

The KPI tile every dashboard opens with: a label, one number, and how that number moved.

It is a Card underneath — the same three surfaces, the same sizes, and the same rule for becoming pressable: give it href or onclick and the whole tile is one target, with the state layer, the ripple and a press that gives a little under the finger.

delta is a fraction, the way a ratio comes out of arithmetic: 0.124 is +12.4%, -0.03 is −3%. The arrow and the colour come from its sign, and invert is for the metrics where down is the good news — churn, refunds, response time.

Charts are not imported here, so the main entry stays light. Put a Sparkline from omaris/chart in the chart snippet and it spans the bottom of the tile.

import { StatCard } from 'omaris'
<StatCard label="Revenue" value={48210} format={{ style: 'currency', currency: 'USD' }}  delta={0.124} deltaLabel="vs last week" />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
header
Label and value on the start side, the icon on the end.
body
No description in the source yet.
label
No description in the source yet.
icon
The icon's tonal container.
value
No description in the source yet.
trend
Delta chip and its label.
delta
The arrow and the percentage, on a tinted pill.
deltaLabel
No description in the source yet.
chart
Wraps the chart snippet; bleeds a little so a sparkline reaches the edges.
footer
No description in the source yet.

Props

label
string

What is being measured — "Revenue", "Active users".

value
string | number

The figure. A number is formatted with format and locale, in Western digits whatever the locale; a string is shown as given, for a value you have already formatted ("4m 12s", "—").

format
Intl.NumberFormatOptions

Intl.NumberFormat options for a numeric value — currency, notation: 'compact', fraction digits. The numbering system is pinned to Western digits on top of whatever you pass.

locale
string

BCP-47 tag for the value and the delta. Defaults to the runtime's.

delta
number

The change, as a fraction: 0.124 is +12.4%, -0.03 is −3%. The arrow and the colour come from its sign; the chip shows its size.

deltaFormat
Intl.NumberFormatOptions

How delta is written. Defaults to a percentage with at most one fraction digit. Pass { style: 'decimal' } for a delta that is a count rather than a ratio ("+3 orders").

deltaLabel
string

What the delta is measured against — "vs last week".

trend
StatCardTrend

Which way it moved. Derived from the sign of delta; set it for a trend you know without a delta to show.

invert

Defaults to false

boolean

Down is the good direction — churn, refunds, latency. Swaps which trend is coloured as success and which as destructive; the arrow still points the way the number went.

variant

Defaults to 'outlined'

CardVariant

Card surface, as on Card.

size

Defaults to 'md'

CardSize

Padding, gap and radius, as on Card; the value's type role steps with it.

tone

Defaults to 'primary'

Tone

The icon container's colour role.

primary
secondary
tertiary
destructive
success
warning
info
loading

Defaults to false

boolean

Swap the value, the delta and the chart for skeletons. The label stays, since you know it already, and the tile keeps its height.

animate

Defaults to false

boolean

Count up to a numeric value on mount, and tween between values as it changes. Off by default, and skipped entirely under reduced motion.

ripple

Defaults to true

boolean

Ripple on press. Pressable tiles only.

class
string
classes
StatCardClasses

Per-part Tailwind overrides. class still covers the root.

icon
Snippet

An icon, drawn in a tonal square at the top end of the tile.

chart
Snippet

A chart along the bottom — a Sparkline from omaris/chart.

footer
Snippet

A last line — a link to the report, when the figure was updated.

shape
ButtonShape

Pill (round) or MD3 rounded rectangle (square).

confetti
boolean | ConfettiOptions

Confetti on press. true for the default burst, or any ConfettiOptions — confetti={{ preset: 'fireworks' }}.

toggle
boolean

Turns the button into a two-state toggle driven by pressed. Ignored when href is set — a link has no pressed state.

pressed bindable
boolean

Selected state of a toggle button. Bindable.

children
Snippet