Skip to main content
WireKit
Copy for LLM

Stat

A metric card for displaying a single key number with optional context: label, trend indicator, period-over-period change, and description. Designed for dashboards and analytics summaries.

Basic Usage

Plain stat
Revenue
€31,200

With Trend Indicator

Use the change + trend props to signal period-over-period movement:

Trend indicator + description
Revenue
€31,200
increased +12% vs last month

Trend Variants

Up, down, neutral
Users
1,250
increased +18% vs last month
Churn
2.4%
decreased +0.3% vs last month
NPS
62
unchanged 0 vs last month

Note: trend="down" uses danger/red color because "going down" in most metrics is negative. For metrics where down is good (churn, errors, bounce rate), flip the semantics in your data layer before passing to the component.

With Icon

Stat with icon
Active Users
842
increased +5% last 30 days

Or use the iconSlot for custom icons:

<x-wirekit::stat label="Revenue" value="€31,200">
    <x-slot:iconSlot>
        <svg>...</svg>
    </x-slot:iconSlot>
</x-wirekit::stat>

Stats Group

Use <x-wirekit::stats> to wrap multiple stat cards in a responsive grid:

Stats group — auto-fit grid
Revenue
€31,200
increased +12%
Users
1,250
increased +8%
Churn
2.4%
decreased +0.3%
NPS
62
unchanged 0

Explicit Columns

Set a fixed number of columns:

Stats group — 3 columns
Revenue
€31,200
Users
1,250
NPS
62

Cascading Stagger

When the children carry animateIn or are wrapped in <x-wirekit::reveal>, the stagger flag cascades their entrances with an incremental delay so each stat card lands a beat after the previous one:

Stats group with 75ms stagger
Revenue
€31,200
increased +12%
Users
1,250
increased +8%
NPS
62
increased +4

stagger (boolean) uses the default 75ms step. Pass an integer for a custom rhythm: :stagger="125". Caps at index 8 to avoid runaway delays. Honors prefers-reduced-motion: reduce (delays collapse to 0).

Stats Group Props

Prop Type Default Description
cols string|null null Fixed columns: '2', '3', '4', '5'. Default: auto-fit (≥200px per card)
stagger bool|int|null null When true, cascades child entrance animations at 75ms steps. Pass an integer to override the step (e.g. :stagger="125").
scope string|null null Scoped personalization key

Citation Footnote

Use the citation prop to attach a small subtle footnote (--text-wk-xs / --color-wk-text-subtle) below the change row — useful for source disclosures or measurement windows.

Stat with citation
Active accounts
12,540
increased +4.2% vs Q4
Source: Internal billing snapshot, March 31 2026

Counter Animation

Set animate to scroll-into-view-trigger a counter animation from 0 to the target value (ease-out cubic, ~1.2s). Respects prefers-reduced-motion: reduce — the value snaps to target with no animation if the OS-level setting is enabled. The static value remains in the DOM as a <span x-text="value"> fallback so search engines, no-JS browsers, and Alpine-pre-init paint all see the real number.

Stat with counter animation on scroll-into-view
Customers
12500
increased +18% this year

The counter runs once when the stat scrolls into view — it does not re-count every time the stat passes the viewport, which would be distracting on a dashboard the reader scrolls through.

To start it again, dispatch wirekit:stat-replay on the stat's root element:

// 1. The stat's root — the element carrying `data-target`.
const stat = document.querySelector('[data-target="12500"]');

// 2. Restart the count-up from zero. Safe to call repeatedly.
stat.dispatchEvent(new CustomEvent('wirekit:stat-replay'));

Under prefers-reduced-motion: reduce the value is already at its target and the event does nothing — a restart is still motion.

When the frames stop arriving

The count-up is driven by requestAnimationFrame, and the browser only runs those callbacks while the document is actually being painted. A stat inside a hidden tab, a zero-sized container or a background tab therefore gets no frames at all — and since a restart resets the value to zero before it counts, that zero would be what a reader sees when the panel finally opens.

The component watches for exactly that. A restart that has not reached its target after the animation's own duration plus a short grace settles the value on the target directly, without animating, so the number is never left standing at zero because the frames never came. Nothing is asked of you: it applies to every restart, and a normal run finishes long before the watch fires and is left alone.

One consequence worth knowing when you script against it: after a restart the value is guaranteed to be correct within roughly a second and a half, whether or not the stat was visible for any of it.

Numeric prefixes and suffixes ($, %, etc.) are preserved through the animation; toLocaleString() formats the in-flight value with locale-aware thousand-separators.

Animation Scope

The animate flag drives the value text only. Every other stat element (label, description, change row, citation, sparkline slot, progress slot, iconSlot) renders statically from first paint and does not participate in the animation cycle.

Element Animated? Rendering
value yes Counts 0 → target via Alpine plugin (1.2s ease-out cubic)
label no Static
description no (default) Static; opt-in to descriptionDeferred (defer fade-in) or descriptionAnimate (synchronous color count-up) — see Three Description Options below
change indicator no Static
citation no Static
iconSlot, sparkline, progress no Static

Layout stability (CLS). The stat reserves vertical space for the final value from first paint via the always-rendered <span x-text="value"> SR-fallback. The count-up does not shift surrounding content — verified to within 1px on the description's vertical position (CLS budget < 0.01) across the animation window.

Screen reader contract. SR users hear the static target value via the <span x-text="value"> fallback. There is no in-flight announcement for the count-up — the canonical SR text is the final number. The description, label, and change row are announced in their static rendered form.

Reduced-motion contract. Under prefers-reduced-motion: reduce, the value snaps to the target instantly (no count-up, no Alpine animation cycle). All description-animation opt-ins (Options A and C below) also degrade to static rendering under reduced motion.

Description behavior during count-up

The description below the value renders statically by default — it is fully visible from first paint, alongside the counting value. Two opt-in props change that behavior when the description should react to the count-up:

Prop Behavior
(none — default) Description renders statically from first paint. Best for accessibility and screen-reader users.
descriptionDeferred Description hides while the counter runs (~1.2s) and fades in once the value settles. Choose when the description is decorative and the reveal-on-settle reads cleaner.
descriptionAnimate Description text color interpolates from --color-wk-text-muted to --color-wk-text synchronously with the count-up. Choose when the description is informationally critical and a subtle reinforcement on landing helps.
<x-wirekit::stat label="Customers" value="12500" description="this year" animate descriptionDeferred />
<x-wirekit::stat label="Customers" value="12500" description="this year" animate descriptionAnimate />

descriptionDeferred and descriptionAnimate are mutually exclusive — passing both throws. Both honor prefers-reduced-motion: reduce and degrade to the static default.

Counter + Entrance Reveal

animate (counter) and animateIn (entrance reveal) compose cleanly. The outer wrapper carries the entrance keyframe, the inner element runs the counter — no Alpine x-data conflicts:

Stat that slides in AND counts up
Customers
12500
increased +18% this year

Sparkline + Progress Slots

Two structural slots — sparkline and progress — sit between the value and the change row. They render their content as-is with no additional styling, so you control the visualization entirely. Pair them with the <x-wirekit::progress> component or any inline SVG/Chart.js render.

The progress slot accepts a <x-wirekit::progress> component — set its variant to color the bar against the value's threshold. A near-quota reading reads better as warning than the neutral default:

Stat with progress slot — quota approaching limit
Quota usage
70 GB

Custom Value via Slot

When a simple string isn't enough, use the default slot:

<x-wirekit::stat label="Revenue">
    <span class="text-indigo-500">€31,200</span>
    <span class="text-sm text-gray-500">EUR</span>
</x-wirekit::stat>

Width & Layout

Individual stats auto-size to their content. Use the <x-wirekit::stats> wrapper with cols to create grid layouts:

<x-wirekit::stats cols="3">
    <x-wirekit::stat label="Users" value="1,204" />
    <x-wirekit::stat label="Revenue" value="$48k" />
    <x-wirekit::stat label="Orders" value="342" />
</x-wirekit::stats>

To constrain the overall width, wrap the stats grid:

<div class="max-w-2xl">
    <x-wirekit::stats cols="2">…</x-wirekit::stats>
</div>

Intent Tiles

Set intent to turn a stat into a KPI tile with an intent-colored border and a faintly tinted body — the dashboard pattern without the hand-rolled inline styles. The tile becomes a labeled role="group" for screen readers.

KPI tiles across intents
Revenue
€421,840
increased +12.4%
Churn
3.2%
decreased -0.4%
Incidents
2

Props

Prop Type Default Description
label string|null null Metric label (small, muted)
value string|null null Primary value (large, prominent)
change string|null null Period-over-period change (e.g. '+12%')
trend string|null null 'up', 'down', 'neutral', or null
icon string|null null Semantic icon alias shown top-right
intent string|null null Color role for the metric — 'primary', 'accent', 'success', 'warning', 'danger', 'info', 'neutral'.
description string|null null Secondary text (e.g. 'vs last month')
descriptionDeferred bool false Render the description only after the value has animated in (pairs with animate).
descriptionAnimate bool false Fade the description in rather than showing it immediately.
citation string|null null Small subtle footnote below the change row (e.g. data source, measurement window)
animate bool false Opt-in counter animation: when true, the value text wraps in an Alpine wirekitStatAnimate handler that animates 0 → target over 1.2s (ease-out cubic) once the stat scrolls 40% into view. Respects prefers-reduced-motion: reduce (snaps to value, no animation). Numeric prefixes / suffixes ($, %) are preserved through the animation.
animateIn string|null null Entrance-reveal preset (e.g. 'fade', 'slide-up'). Composes cleanly with animate: the outer wrapper carries the entrance reveal, the inner element keeps the counter handler.
scope string|null null Scoped personalization name

Slots

Slot Purpose
default Custom value rendering (used when value prop is null)
iconSlot Custom icon/SVG (overrides icon prop)
sparkline Inline visualization between the value and the change row (no built-in styling)
progress Inline progress indicator between the value and the change row (no built-in styling)

Accessibility

  • The trend arrow glyph (▲/▼/→) is marked aria-hidden="true" — its meaning is duplicated by an sr-only span ("increased", "decreased", "unchanged") so screen readers announce the direction without relying on visual glyphs
  • Use the card inside a semantic grid/section landmark for proper document outline
  • Value text uses tabular-nums (monospace digits) so trailing decimals in grids align visually

Keyboard Interaction

This component is purely presentational and does not respond to keyboard input.

Pitfalls

  • Don't use stat for prose. It carries <dt> / <dd> semantics — screen readers announce as a definition list. For inline metrics in flowing copy, use <x-wirekit::heading> + <x-wirekit::text>.

Design Tokens

Element Token
Card background --color-wk-bg-elevated
Card border --color-wk-border
Card radius --radius-wk-lg
Card padding --padding-wk-x-lg / --padding-wk-y-lg
Label color --color-wk-text-muted
Value color --color-wk-text
Trend up color --color-wk-success
Trend down color --color-wk-danger-text
Trend neutral color --color-wk-text-muted
Icon color --color-wk-text-subtle

Customization

Override defaults without publishing views via config/wirekit.php:

'components' => [
    'stat' => [],
],

Further Reading

Was this page helpful?

Thanks — that helps.

Voting requires cookies or local storage. What we store