Skip to content
ShineUI
Install

StatCard

One number that matters.

<sui-stat-card><sui-sparkline>DataStable

About

Dashboard KPI card: an uppercase label, one large tabular number, a delta with a direction arrow in good, bad or neutral tone, a note, an optional inline sparkline and a drill link. Surface card, hairline border, no resting shadow. Use one per metric in the row across the top of a dashboard.

Usage

<sui-stat-card label="Receivables" value="$90,740.45" delta="+12.4%" delta-direction="up" note="vs. July"></sui-stat-card> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-stat-card"); el.data = [42, 48, 45, 52, 49, 58, 61]; /* number[] | undefined */ el.addEventListener("suiLink", (e) => console.log(e.detail)); </script>
import { SuiStatCard } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const data = [42, 48, 45, 52, 49, 58, 61]; /* number[] | undefined */ return <SuiStatCard label="Receivables" value="$90,740.45" delta="+12.4%" deltaDirection="up" note="vs. July" data={data} onSuiLink={(e) => console.log(e.detail)} />; }
<script setup> import { SuiStatCard } from "@shineui/vue"; import "@shineui/core/shineui.css"; const data = [42, 48, 45, 52, 49, 58, 61]; // number[] | undefined const onChange = (e) => console.log(e.detail); </script> <template> <SuiStatCard label="Receivables" value="$90,740.45" delta="+12.4%" delta-direction="up" note="vs. July" :data="data" @sui-link="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiStatCard } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiStatCard], template: `<sui-stat-card label="Receivables" value="$90,740.45" delta="+12.4%" delta-direction="up" note="vs. July" [data]="data" (suiLink)="onChange($event)"></sui-stat-card>`, }) export class Example { data = [42, 48, 45, 52, 49, 58, 61]; // number[] | undefined onChange(e: CustomEvent) { console.log(e.detail); } }

KPI row

label, value, delta and note are attributes; data, the sparkline series, is a property. Format the value before passing it; the card does no math.

<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:14px"> <sui-stat-card id="kpi-ar" label="Receivables" value="$90,740.45" delta="+12.4%" delta-direction="up" note="vs. July" href="#"></sui-stat-card> <sui-stat-card id="kpi-cash" label="Cash on hand" value="$87,929.18" delta="+4.1%" delta-direction="up" note="vs. July"></sui-stat-card> <sui-stat-card label="Overdue" value="$20,220.00" delta="2 invoices" accent note="oldest 15 days"></sui-stat-card> </div>
// setup(root): properties and events setup(root) { const ar = root.querySelector('#kpi-ar'); ar.severity = 'critical'; ar.data = trend.receivables; root.querySelector('#kpi-cash').data = trend.cash; }

size="lg" draws the value at 30px, the step above a page title, for the figures a dashboard opens with. href makes the whole card one link with the row as its label; command makes it the control with no href. suiLink fires on either before the browser follows, so a host that navigates in memory calls preventDefault.

Activate a card. The first two are links; the third has no href and asks the page to navigate.
<div style="display:grid;gap:10px"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px"> <sui-stat-card size="lg" label="Open tasks" value="34" delta="+3" delta-direction="up" note="since Monday" href="#" link-label="View tasks"></sui-stat-card> <sui-stat-card size="lg" label="Overdue" value="3" delta="+1" delta-direction="up" severity="critical" note="1 of them yours" href="#" link-label="View tasks"></sui-stat-card> <sui-stat-card id="kpi-command" size="lg" label="Done this week" value="12" delta="+2" delta-direction="up" note="vs. last week" command></sui-stat-card> </div> <span id="kpi-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Activate a card. The first two are links; the third has no href and asks the page to navigate.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#kpi-log'); root.addEventListener('suiLink', (e) => { e.preventDefault(); const card = e.target.closest('sui-stat-card'); log.textContent = e.detail.href ? `${card.label}: the page would follow ${e.detail.href}; prevented here.` : `${card.label}: no href, the page navigates itself.`; }); }

Good and bad deltas

severity sets the tone. Positive takes the positive ink, green under semantic status. Critical takes the danger ink, so rising receivables read as a warning. Neutral takes the muted ink at regular weight and keeps its arrow. Critical also rings its arrow and adds a hidden "unfavorable", so color is never the only cue.

<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:14px"> <sui-stat-card label="Revenue" value="$412,880.00" delta="+8.2% vs. July" delta-direction="up" note="7 months"></sui-stat-card> <sui-stat-card label="Receivables" value="$90,740.45" delta="+12.4% vs. July" delta-direction="up" severity="critical" note="collection is slowing"></sui-stat-card> <sui-stat-card label="Days sales outstanding" value="41 days" delta="down 3 days" delta-direction="down" note="target 35"></sui-stat-card> </div>

Shared scale

Each sparkline normalizes to its own series unless scale-min and scale-max are set. Set the same pair on every card in a row, or the shapes read as comparable when they are not.

<div style="display:grid;gap:14px"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px"> <sui-stat-card id="own-1" label="Operating" value="$87,929.18"></sui-stat-card> <sui-stat-card id="own-2" label="Payroll" value="$21,004.90"></sui-stat-card> <sui-stat-card id="own-3" label="Reserve" value="$8,410.00"></sui-stat-card> </div> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px"> <sui-stat-card id="fix-1" label="Operating" value="$87,929.18" scale-min="0" scale-max="100"></sui-stat-card> <sui-stat-card id="fix-2" label="Payroll" value="$21,004.90" scale-min="0" scale-max="100"></sui-stat-card> <sui-stat-card id="fix-3" label="Reserve" value="$8,410.00" scale-min="0" scale-max="100"></sui-stat-card> </div> </div>
// setup(root): properties and events setup(root) { const rows = [trend.operating, trend.payroll, trend.reserve]; rows.forEach((data, i) => { root.querySelector(`#own-${i + 1}`).data = data; root.querySelector(`#fix-${i + 1}`).data = data; }); }

Sparkline alone

sui-sparkline is the trend part of the card, usable beside any figure; accent marks the one highlighted trend on a view. It is aria-hidden and draws no axes or tooltip, so the sentence next to it has to say what the trend is.

Cash on hand rose in five of the last seven periods.
Receivables climbed 45 percent over the same periods.
The same receivables series on a fixed 0 to 100 scale, which is how it compares with the row above.
<div style="display:grid;gap:12px;font:460 13px/1.6 var(--font-sans);color:var(--ink)"> <div style="display:flex;align-items:center;gap:12px"> <sui-sparkline id="sp-plain"></sui-sparkline> <span>Cash on hand rose in five of the last seven periods.</span> </div> <div style="display:flex;align-items:center;gap:12px"> <sui-sparkline id="sp-accent" accent width="140" height="34"></sui-sparkline> <span>Receivables climbed 45 percent over the same periods.</span> </div> <div style="display:flex;align-items:center;gap:12px"> <sui-sparkline id="sp-fixed" min="0" max="100"></sui-sparkline> <span style="color:var(--ink-muted)">The same receivables series on a fixed 0 to 100 scale, which is how it compares with the row above.</span> </div> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sp-plain').data = trend.cash; root.querySelector('#sp-accent').data = trend.receivables; root.querySelector('#sp-fixed').data = trend.receivables; }

Points arrivesui-sparkline

data is a property, and assigning a new array redraws the line; pushing into the old one does not. With fewer than two points the element draws nothing, so a feed that has just started shows no stray dot. min and max hold the scale steady as points arrive.

No readings yet.
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;font:460 13px/1.6 var(--font-sans);color:var(--ink)"> <span style="display:inline-flex;width:96px;height:28px;align-items:center"><sui-sparkline id="spk-live" min="0" max="100"></sui-sparkline></span> <span id="spk-count" style="min-width:220px">No readings yet.</span> <sui-button id="spk-add" variant="outline" size="sm" label="Add a reading"></sui-button> <sui-button id="spk-reset" variant="ghost" size="sm" label="Reset"></sui-button> </div>
// setup(root): properties and events setup(root) { const spark = root.querySelector('#spk-live'); const count = root.querySelector('#spk-count'); const readings = [42, 48, 45, 57, 63, 60, 71, 78, 74, 86, 91, 88]; let n = 0; const draw = () => { spark.data = readings.slice(0, n); count.textContent = n === 0 ? 'No readings yet.' : n === 1 ? 'One reading: nothing to draw yet.' : `${n} readings, the latest ${readings[n - 1]}.`; }; root.querySelector('#spk-add').addEventListener('click', () => { if (n < readings.length) { n += 1; draw(); } }); root.querySelector('#spk-reset').addEventListener('click', () => { n = 0; draw(); }); }

Keyboard

KeyAction
Tab sui-stat-cardLands on a card with an href or command. A plain card is not a tab stop.
Enter Space sui-stat-cardEmits suiLink, then follows the href unless a listener called preventDefault(). Space works on a linked card too, which a plain link does not do.

API

<sui-stat-card>

Properties

PropertyAttributeTypeDefaultDescription
accentaccentbooleanfalseAccent sparkline: the one highlighted trend on the view. Reflected.
commandcommandbooleanfalseMakes the whole card the control without an href: a button announced as a link, for a host that navigates itself on suiLink. Draws no link row. With href set the card is already the control and this changes nothing. Reflected.
dataproperty onlynumber[]number[] | undefinedSeries for the inline sparkline, oldest first. Property, not attribute.
deltadeltastringstring | undefinedChange text ("+12.4%", "2 open").
deltaDirectiondelta-direction"down" | "up" | undefinedAdds an up or down arrow before the delta. Reflected.
hrefhrefstringstring | undefinedDrill-down href, the word every item shape in the library uses. The whole card becomes one link, with the link row as its visible label, so the card is one tab stop and a middle click opens it fresh.
labellabelstring''Metric name, rendered as an uppercase micro-label ("Receivables").
linkLabellink-labelstringstring | undefinedLink text; an arrow is appended.
notenotestringstring | undefinedFaint context after the delta ("vs. July").
scaleMaxscale-maxnumbernumber | undefinedSparkline scale ceiling. See scaleMin.
scaleMinscale-minnumbernumber | undefinedSparkline scale floor. Set the same pair on every card in a row so the trends compare; omitted, each card's sparkline normalizes to its own series and the shapes mislead.
severityseveritySuiTrendSeverity"critical" | "neutral" | "positive"'positive'How the change reads, on the status ladder sui-trend shares: positive, good news, in the positive ink (the ink under brand status, green under semantic); critical, bad news (a rising receivable), in the danger ink with the direction arrow ringed and a visually hidden "unfavorable" after the figure; neutral, neither, in the muted ink at regular weight, the arrow kept and unringed. So the judgement never rests on colour alone. The danger ink is the brand's own under brand mode and rose under semantic status mode, the same ink an overdue date wears. An unknown value reads as positive. Reflected.
sizesize"lg" | "md"'md'Value size: md draws the figure at 23px, lg at 30px for the aggregate figures a dashboard opens with, the step above a page title. The label, delta and note keep their sizes.
valuevaluestring''Formatted figure ("$90,740.45"): two-decimal currency with separators, parentheses for report negatives. Format before passing; the card does no math.

Events

EventDetailReactVueDescription
suiLinkSuiStatCardLinkDetailonSuiLink@sui-linkFires when the card is activated by click, Enter or Space, before the browser follows href; detail is { href }. Cancelable: preventDefault() keeps the page where it is, for a host with in-memory navigation.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the card when it is a control, a link or a command card; does nothing otherwise.

Types

SuiStatCardLinkDetail interfaceDetail of suiLink: the card's href, or undefined on a command card that has none.
href?string
SuiTrendSeverity typeHow a change reads, on the library's status ladder: positive is good news, critical bad news, neutral neither.
No members.

<sui-sparkline>

Properties

PropertyAttributeTypeDefaultDescription
accentaccentbooleanfalseOrange line for the one highlighted trend on the view; otherwise the data-viz ramp's --viz-2. Set --sui-sparkline-line on the element or any ancestor to paint the line and its end point in a color of your own; it wins over accent, as a series color does on the charts. Any CSS color or var(--token) works; use a token so the color follows the theme. Reflected.
dataproperty onlynumber[][]Series values, oldest first. Property, not attribute.
heightheightnumber28SVG height in px.
maxmaxnumbernumber | undefinedScale ceiling. Omitted, the series' own maximum is used. See min.
minminnumbernumber | undefinedScale floor. Omitted, the series' own minimum is used, so each sparkline normalizes to itself; set the same min and max on every sparkline in a row so they compare.
widthwidthnumber96SVG width in px.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

<sui-stat-card>

<sui-sparkline>

Every name above is listed with its value and its role in Foundations.

See also