KPI Cards
Compact cards for a single headline figure and its context. Every variant below is lifted from a page this project ships, so the markup you copy is the markup that renders. Each snippet is the card alone — drop it into any grid. The last section documents the grid.
Basic
Used in sales-dashboard.htmlLabel, value and a supporting line. The smallest useful KPI card.
Total Revenue
$48,290
Across 1,284 orders
<div class="rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<p class="text-sm text-muted">Total Revenue</p>
<p class="mt-2 text-xl md:text-2xl font-bold text-heading num-display">$48,290</p>
<p class="mt-2 text-xs text-muted">Across 1,284 orders</p>
</div>
Icon and delta
Used in hr-dashboard.htmlA tinted icon identifies the metric and a signed delta states its direction. The arrow and the colour agree, so the change reads without relying on colour alone.
837
Total Clients
<div class="rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 items-center gap-3">
<span
class="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-chart-1/10 text-chart-1"
aria-hidden="true"
>
<i class="ph ph-users-three text-2xl"></i>
</span>
<div class="min-w-0">
<p class="text-2xl font-bold text-heading num-display">837</p>
<p class="mt-0.5 text-sm leading-snug text-muted">Total Clients</p>
</div>
</div>
<span class="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-success-on-soft num-display">
<i class="ph ph-trend-up text-sm" aria-hidden="true"></i>14.20%
</span>
</div>
</div>
Pendulum badge
Used in sales-dashboard.html
A delta pill hanging from a thread, gently swinging. The icon, the
three-up footer and the pill all share one accent colour.
motion-reduce:animate-none
stops the motion for anyone who asked the OS to reduce it.
Stores have sold
$331,224
Outlets
4
Orders
8.2K
Growth
+31%
<div class="group relative overflow-hidden rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<!-- The thread the pill swings from. -->
<div class="absolute right-4 top-0 origin-top animate-swing motion-reduce:animate-none">
<div class="h-8 w-px bg-success"></div>
<div class="flex h-12 w-12 -translate-x-1/2 -translate-y-0.5 items-center justify-center rounded-full bg-success text-slate-900">
<span class="text-xs font-semibold num-display">+31.2%</span>
</div>
</div>
<div class="relative">
<div class="mb-6 flex size-10 items-center justify-center rounded-full bg-success-soft">
<i class="ph ph-storefront text-base text-success-on-soft"></i>
</div>
<p class="mb-2 text-sm text-muted">Stores have sold</p>
<p class="mb-4 text-xl md:text-2xl font-bold text-heading num-display">$331,224</p>
<div class="grid grid-cols-3 gap-2">
<div class="rounded-lg bg-subtle p-2 text-center">
<p class="text-xs text-text-secondary">Outlets</p>
<p class="font-bold num-display text-text-secondary">4</p>
</div>
<div class="rounded-lg bg-subtle p-2 text-center">
<p class="text-xs text-text-secondary">Orders</p>
<p class="font-bold num-display text-text-secondary">8.2K</p>
</div>
<div class="rounded-lg bg-success-soft p-2 text-center">
<p class="text-xs text-success-on-soft">Growth</p>
<p class="font-bold num-display text-success-on-soft">+31%</p>
</div>
</div>
</div>
</div>
Sparkline
Used in customer-list.html
An ApexCharts bar sparkline over the last eight periods. The final
bar is the headline figure above it, so the two cannot disagree.
Add an entry to
CARDS
in
customer-kpi-charts.js
keyed by the mount id, with a
data
array.
Total Customer
876
+54
New
3.1y
Avg tenure
4.2%
Growth
<div class="flex flex-col rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 items-center gap-3">
<span
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-chart-1/10 text-chart-1 ring-1 ring-chart-1/20"
aria-hidden="true"
>
<i class="ph ph-users-three text-lg"></i>
</span>
<div class="min-w-0">
<p class="text-sm text-muted">Total Customer</p>
<p class="mt-1 text-xl md:text-2xl font-bold leading-none tracking-tight text-heading num-display">876</p>
</div>
</div>
<span class="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border px-2 py-0.5 text-xs font-medium text-text-secondary">
Live
</span>
</div>
<!-- Mount point. Apex has no content of its own, so the box needs an
explicit height. -->
<div class="mb-4 mt-4 h-14 w-full">
<div
id="kpi-demo-spark"
class="h-full w-full"
role="img"
aria-label="Total Customer, last 8 months: Nov 612 rising to Jun 876"
></div>
</div>
<div class="mt-auto grid grid-cols-3 gap-2 border-t border-border-subtle pt-3 text-center">
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-heading num-display">+54</p>
<p class="mt-0.5 text-xs leading-tight text-muted">New</p>
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-heading num-display">3.1y</p>
<p class="mt-0.5 text-xs leading-tight text-muted">Avg tenure</p>
</div>
<div class="min-w-0">
<p class="flex items-center justify-center gap-1 truncate text-sm font-semibold text-success-on-soft num-display">
<i class="ph ph-trend-up text-xs" aria-hidden="true"></i>4.2%
</p>
<p class="mt-0.5 text-xs leading-tight text-muted">Growth</p>
</div>
</div>
</div>
Tick meter
Used in hr-dashboard.html
Full-height ticks where the filled proportion is at full strength
and the rest is faded, so the row reads as progress toward a
target rather than as a series. Same component as the sparkline,
declared with
shape: "meter"
and a
pct.
324
Attendance
of 348 staff present
<div class="flex flex-col rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 items-center gap-3">
<span
class="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-chart-3/10 text-chart-3"
aria-hidden="true"
>
<i class="ph ph-fingerprint text-2xl"></i>
</span>
<div class="min-w-0">
<p class="text-2xl font-bold text-heading num-display">324</p>
<p class="mt-0.5 text-sm leading-snug text-muted">Attendance</p>
</div>
</div>
<span class="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-success-on-soft num-display">
<i class="ph ph-trend-up text-sm" aria-hidden="true"></i>12.32%
</span>
</div>
<div class="mt-4 h-8 w-full">
<div
id="kpi-demo-meter"
class="h-full w-full"
role="img"
aria-label="Attendance: 324 of 348 staff present, 93 percent"
></div>
</div>
<p class="mt-2 text-xs text-muted num-display">of 348 staff present</p>
</div>
Meter above the label
Used in crypto-dashboard.htmlThe same meter placed above the metric instead of below it, which suits a tile that leads with its chart. Mount id and configuration are identical — only the position in the card changes.
Market Cap
$2.38T
<div class="rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<div class="h-9 w-full">
<div
id="kpi-demo-meter-top"
class="h-full w-full"
role="img"
aria-label="Market Cap: 65 percent of the all-time high"
></div>
</div>
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
<div class="flex min-w-0 items-center gap-3">
<span
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-chart-4/10 text-chart-4"
aria-hidden="true"
>
<i class="ph ph-chart-pie-slice text-lg"></i>
</span>
<div class="min-w-0">
<p class="truncate text-sm text-muted">Market Cap</p>
<p class="mt-0.5 text-xl md:text-2xl font-bold leading-none tracking-tight text-heading num-display">$2.38T</p>
</div>
</div>
<span class="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-success-on-soft num-display">
<i class="ph ph-trend-up text-sm" aria-hidden="true"></i>2.4%
</span>
</div>
</div>
Progress ring
Used in crypto-market.html
An inline SVG ring for a single share-of-whole figure.
stroke-dasharray
is the arc length against the circumference — a measurement,
not a decoration. The percentage is real text inside the ring, and
the SVG is
aria-hidden
so the figure is read rather than inferred from an arc. No
JavaScript.
Total market cap
$1.44 T
<div class="flex flex-col rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="flex items-center gap-1.5 text-sm text-muted">
<i class="ph ph-globe-hemisphere-west text-base text-chart-1" aria-hidden="true"></i>
Total market cap
</p>
<p class="mt-2 flex items-baseline gap-1">
<span class="text-xl md:text-2xl font-bold leading-none tracking-tight text-heading num-display">$1.44</span>
<span class="text-sm font-medium text-muted num-display">T</span>
</p>
</div>
<span
class="relative inline-flex shrink-0 items-center justify-center"
role="img"
aria-label="Top 4 assets: $1320.00B of $1442.29B, 92 percent"
>
<svg viewBox="0 0 64 64" class="size-16 shrink-0 -rotate-90" aria-hidden="true">
<circle cx="32" cy="32" r="26" fill="none" stroke="currentColor" stroke-width="7" class="text-subtle" />
<circle
cx="32" cy="32" r="26" fill="none" stroke="currentColor" stroke-width="7"
stroke-linecap="round"
stroke-dasharray="150.29 13.07"
class="text-chart-1"
/>
</svg>
<span class="absolute text-xs font-semibold text-heading num-display">92%</span>
</span>
</div>
<div class="mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-border-subtle pt-3">
<span class="text-xs text-muted">Top 4 assets</span>
<span class="text-xs font-semibold text-heading num-display">$1320.00B / $1442.29B</span>
</div>
</div>
Area chart hero
Used in coin.html
A ranked card with a full-bleed area line and a three-metric
footer. The line uses
preserveAspectRatio="none"
so it spans any width, with
vector-effect="non-scaling-stroke"
keeping the stroke crisp. Flat tint, no gradient.
- Search
- +412%
- Volume
- $684.2M
- Cap
- $2.84B
<div class="flex flex-col rounded-3xl bg-surface p-4 md:p-5 ring-1 ring-black/5 transition-shadow hover:shadow-lg dark:ring-white/10">
<div class="flex items-center gap-3">
<span class="relative shrink-0">
<span
class="flex size-11 shrink-0 items-center justify-center rounded-full bg-chart-4/10 text-chart-4 ring-1 ring-chart-4/25"
aria-hidden="true"
>
<i class="ph ph-lightning text-lg"></i>
</span>
<span class="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-surface text-[10px] font-bold text-heading ring-1 ring-border num-display">
1<span class="sr-only">, trending rank</span>
</span>
</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-semibold text-heading">Injective</span>
<span class="mt-0.5 block truncate text-xs text-muted num-display">INJ / USDT</span>
</span>
<button
type="button"
aria-label="Add Injective to watchlist"
aria-pressed="false"
class="inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted transition-colors hover:bg-subtle hover:text-heading focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
>
<i class="ph ph-star text-base" aria-hidden="true"></i>
</button>
</div>
<div class="mt-4 flex items-end justify-between gap-3">
<span class="min-w-0 text-2xl font-bold leading-none tracking-tight text-heading num-display">$28.42</span>
<span class="inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ring-success/30 text-success-on-soft num-display">
<i class="ph ph-arrow-up text-xs" aria-hidden="true"></i>9.64%
</span>
</div>
<div class="mt-3">
<svg viewBox="0 0 100 34" preserveAspectRatio="none" class="block h-10 w-full text-success-on-soft" fill="none" role="img" aria-label="INJ trend, up">
<path d="M0 30 L14 27 L29 25 L43 20 L57 17 L71 11 L86 8 L100 4 L100 34 L0 34 Z" class="fill-current opacity-10" stroke="none" />
<path d="M0 30 L14 27 L29 25 L43 20 L57 17 L71 11 L86 8 L100 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" />
</svg>
</div>
<dl class="mt-auto grid grid-cols-3 gap-2 border-t border-border-subtle pt-3">
<div class="min-w-0">
<dt class="truncate text-xs text-muted">Search</dt>
<dd class="mt-1 truncate text-sm font-semibold text-success-on-soft num-display">+412%</dd>
</div>
<div class="min-w-0 text-center">
<dt class="truncate text-xs text-muted">Volume</dt>
<dd class="mt-1 truncate text-sm font-semibold text-heading num-display">$684.2M</dd>
</div>
<div class="min-w-0 text-right">
<dt class="truncate text-xs text-muted">Cap</dt>
<dd class="mt-1 truncate text-sm font-semibold text-heading num-display">$2.84B</dd>
</div>
</dl>
</div>
Card grid
Used in every dashboard
The wrapper the cards above sit in. Pick steps that divide the
card count evenly — four cards on a
lg:grid-cols-3
step strand one alone on a second row.
1 → 2 → 4
all divide four.
Revenue
$48,290
+12.4% vs last month
Orders
1,284
+3.1% vs last month
Refunds
24
−1.8% vs last month
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<p class="text-sm text-muted">Revenue</p>
<p class="mt-2 text-xl md:text-2xl font-bold text-heading num-display">$48,290</p>
<p class="mt-2 text-xs text-success-on-soft num-display">+12.4% vs last month</p>
</div>
<div class="rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<p class="text-sm text-muted">Orders</p>
<p class="mt-2 text-xl md:text-2xl font-bold text-heading num-display">1,284</p>
<p class="mt-2 text-xs text-success-on-soft num-display">+3.1% vs last month</p>
</div>
<div class="rounded-3xl bg-surface p-4 md:p-6 ring-1 ring-black/5 dark:ring-white/10">
<p class="text-sm text-muted">Refunds</p>
<p class="mt-2 text-xl md:text-2xl font-bold text-heading num-display">24</p>
<p class="mt-2 text-xs text-danger-on-soft num-display">−1.8% vs last month</p>
</div>
</div>
Guidance
Conventions these cards follow across the project.
-
Derive, don't retype
A card's footer should be computed from the same figures as its headline. When both are typed by hand they drift apart, and the card ends up contradicting itself.
-
Charts need a measured height
An ApexCharts mount has no content of its own. Give the wrapper an explicit height —
h-14for a sparkline,h-8for a meter. A percentage height against an auto-height parent collapses to zero. -
Keep rows divisible
Pick grid steps that divide the card count evenly. Four cards on a
lg:grid-cols-3step strand one alone on a second row, leaving a visible gap. -
Colour is never the only signal
Pair every delta colour with an arrow and a sign. A red figure alone is unreadable to anyone who cannot separate it from green.