KPI Cards · Six Flavours
One SaaS metrics dataset, six ready-to-use scorecards — headline, goal ring, multi headline, multi-delta strip, drivers, and forecast.
What you’re looking at
Six ready-to-use KPI scorecards, each built from one synthetic SaaS dataset — daily revenue, targets, orders, active users, and support metrics for three product lines across the first half of 2026. Switch tabs to move between them:
- Headline + Delta: the big number with a month-over-month change chip.
- Goal Ring: revenue against a target, drawn as a progress ring coloured by how close you are.
- Multi Headline: three focal numbers side by side.
- Multi-Delta Strip: one value compared against several past windows at once.
- Drivers: which product lines moved the number, as contributor chips.
- Forecast: monthly actuals extended by a Holt linear-trend projection with a confidence band.
How it works
Every number on these cards comes straight from Muze’s DataModel: select(...) scopes the rows by date and groupBy([]) rolls a measure up to a single value — the same query engine that powers every Muze chart, here driving plain HTML. Each card then computes its figures with a few small helpers and writes them into a fixed HTML scaffold by setting textContent on slots like .kpi-value and .kpi-delta-text.
The HTML scaffold and CSS are shared across all six cards and never change — only the JavaScript differs, and each example carries just the helpers it needs. Copy the three artifacts below into the matching Muze Studio panels, load the CSV into a ThoughtSpot Worksheet, and edit the hardcoded measures, dates, and labels to fit your own data.
Take it with you
The JavaScript below is the complete, self-contained code for the selected example (“Headline + Delta”) — switch tabs above to see a different one. The HTML and CSS are shared by all six. Paste each piece into the matching Muze Studio panel.
JavaScript
Self-contained: reads the DataModel, computes the values with inlined helpers, and writes them into the scaffold. Edit the measures, date ranges, and labels to fit your data — there's no spec or shared library to learn.
Preview
const { muze, getDataFromSearchQuery, formatters, events } = viz;
const dm = getDataFromSearchQuery();
const card = document.querySelector("#chart .kpi-card");
// Compact USD formatter — $1.3M, $12.4K.
const fmt = new formatters.NumberFormatter("currency", { currency: "USD", unit: "auto", decimalDigits: 1 });
const money = (n) => (n == null ? "—" : fmt.format(n));
// Sum a measure between two dates (inclusive). The column's default
// aggregation applies — "revenue" sums. "date" is an epoch timestamp.
function aggregate(field, fromISO, toISO) {
const C = muze.DataModel.ComparisonOperators;
const L = muze.DataModel.LogicalOperators;
const scoped = dm.select({
conditions: [
{ field: "date", value: Date.parse(fromISO + "T00:00:00Z"), operator: C.GREATER_THAN_EQUAL },
{ field: "date", value: Date.parse(toISO + "T23:59:59Z"), operator: C.LESS_THAN_EQUAL },
],
operator: L.AND,
});
const out = scoped.groupBy([]).getData();
if (!out.data.length) return null;
return Number(out.data[0][out.schema.findIndex((c) => c.name === field)]);
}
const thisMonth = aggregate("revenue", "2026-06-01", "2026-06-30");
const lastMonth = aggregate("revenue", "2026-05-01", "2026-05-31");
const change = (thisMonth - lastMonth) / Math.abs(lastMonth);
// Title.
card.querySelector(".kpi-title").textContent = "Revenue This Month vs Last Month";
// Headline value — clone the headline template into the primary row.
const headline = card
.querySelector("template.kpi-primary-tmpl-headline")
.content.firstElementChild.cloneNode(true);
card.querySelector(".kpi-primary-row").appendChild(headline);
headline.querySelector(".kpi-value").textContent = money(thisMonth);
// Delta chip — green when up (good for revenue), red when down.
card.classList.add("kpi-card--has-delta");
const delta = card.querySelector(".kpi-row-delta");
const up = change >= 0;
const color = up ? "#15803d" : "#b91c1c";
delta.querySelector(".kpi-delta-arrow").textContent = up ? "▲" : "▼";
delta.querySelector(".kpi-delta-text").textContent = (Math.abs(change) * 100).toFixed(1) + "%";
delta.querySelector(".kpi-delta").style.background = color + "1A";
delta.querySelector(".kpi-delta").style.color = color;
delta.querySelector(".kpi-sublabel").textContent = "vs last month";
events.emitRenderCompletedEvent();
CSS
The shared card chassis — design tokens on .kpi-card plus every layout primitive. Identical for all six cards; theme via the --kpi-* custom properties.
Preview
/* ──────────────────────────────────────────────────────────────────────
KPI runtime stylesheet — embedded verbatim, deterministic layout.
Design tokens live on '.kpi-card' so any consumer can theme by
overriding the relevant '--kpi-*' custom property.
──────────────────────────────────────────────────────────────────────*/
.kpi-card {
/* Card chrome */
--kpi-card-bg: #ffffff;
--kpi-card-border: #e5e7eb;
--kpi-card-radius: 14px;
--kpi-card-padding: 22px 24px;
--kpi-card-gap: 10px;
--kpi-card-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 6px 24px rgba(15,23,42,0.06);
/* Typography */
--kpi-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Helvetica, Arial, sans-serif;
--kpi-title-color: #6b7280;
--kpi-subtitle-color: #94a3b8;
--kpi-value-color: #0f172a;
--kpi-muted-color: #94a3b8;
/* Responsive headline scales — separate for bare tile vs in-ring.
The in-ring scale is tuned to feel proportional to the ring's
own responsive range (clamp 220-360px below): at the small ring
the headline reads ~28px, at the 360px ring it sits at ~44px so
the focal number visually matches the ring's size. */
--kpi-headline-size: clamp(36px, 6vw, 52px);
--kpi-headline-size-ring: clamp(28px, 3.6vw, 44px);
/* Ring */
--kpi-ring-size: clamp(220px, 50%, 360px);
--kpi-ring-track: #f1f5f9;
--kpi-ring-text-gap: 6px;
--kpi-accent: #2563eb;
/* Strip */
--kpi-strip-min-cell: 56px;
--kpi-strip-gap: 1px;
/* Apply chrome tokens. */
font-family: var(--kpi-font-family);
background: var(--kpi-card-bg);
border: 1px solid var(--kpi-card-border);
border-radius: var(--kpi-card-radius);
box-shadow: var(--kpi-card-shadow);
padding: var(--kpi-card-padding);
display: flex; flex-direction: column; gap: var(--kpi-card-gap);
position: relative;
}
.kpi-title { font-size: 12px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--kpi-title-color); }
.kpi-subtitle { font-size: 11px; color: var(--kpi-subtitle-color); }
.kpi-value { font-size: var(--kpi-headline-size); font-weight: 700; line-height: 1.05; color: var(--kpi-value-color); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
/* Primary row: a flex container the renderer fills by cloning the
matching per-kind <template> for each entry in spec.config.kpi.primary.
When the spec has ONE primary, one cell renders full-width; when
it has many, the row distributes them with equal flex shares. The
first-child / nth-of-type approach keeps the layout responsive
without hardcoding cell widths. */
.kpi-primary-row { display: flex; flex-direction: row; align-items: flex-start; gap: 24px; }
.kpi-primary-row .kpi-primary { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.kpi-primary-row .kpi-primary-goal-ring { display: block; }
.kpi-primary-row .kpi-primary-goal-bar { display: flex; flex-direction: column; gap: 10px; }
/* Per-cell label appears above the focal element when an array-form
primary cell wants a small caption (e.g. "MTD" / "Last Month").
Hidden when empty so single-primary cards don't reserve space. */
.kpi-primary-label { font-size: 11px; color: var(--kpi-title-color); font-weight: 600; }
.kpi-primary-label:empty { display: none; }
/* When the row carries MORE than one primary cell, the bare headline
value would overflow at full size — scale it down. The selector
':has(.kpi-primary + .kpi-primary)' matches when there are at least
two primary siblings in the row. */
.kpi-primary-row:has(.kpi-primary + .kpi-primary) .kpi-value { font-size: clamp(20px, 2.5vw, 32px); }
/* Row + trend reveal rules unchanged. */
.kpi-rows .kpi-row { display: none; }
.kpi-card--has-delta .kpi-row-delta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
.kpi-card--has-multi-delta-strip .kpi-row-multi-delta-strip { display: block; }
.kpi-card--has-drivers .kpi-row-drivers { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; margin-top: 6px; }
.kpi-card--has-narrative .kpi-row-narrative { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
.kpi-card--has-sublabel .kpi-row-sublabel { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
.kpi-card--has-status-chip .kpi-row-status-chip { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
/* Trend mount stays out of layout flow until activated — otherwise it
reserves a 70px-tall blank box at the bottom of every card. */
.kpi-trend { display: none; }
.kpi-card--has-trend .kpi-trend { display: block; }
/* Collapse always-rendered text wrappers when they're empty so the
card gap doesn't reserve space for invisible titles / subtitles. */
.kpi-title:empty, .kpi-subtitle:empty { display: none; }
.kpi-delta { display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
.kpi-sublabel, .kpi-sublabel-text { font-size: 12px; color: var(--kpi-muted-color); }
/* Inline multi-delta strip: bold colored value + muted "vs X" label,
adjacent cells separated by a thin 1px divider. No outer border,
no per-cell background — reads as inline text, not as a table.
Wraps gracefully on narrow cards. */
.kpi-strip { width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 18px 22px; margin-top: 6px; }
.kpi-strip-cell { display: inline-flex; align-items: baseline; gap: 8px; position: relative; padding-left: 22px; }
.kpi-strip-cell:first-child { padding-left: 0; }
.kpi-strip-cell:not(:first-child)::before { content: ""; position: absolute; left: 0; top: 8%; bottom: 8%; width: 1px; background: var(--kpi-card-border); }
.kpi-strip-period { font-size: 12px; color: var(--kpi-muted-color); font-weight: 400; }
.kpi-strip-delta { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
/* Drivers row: the section-header + chip-strip stacking comes from
the has-drivers reveal rule above. The label mirrors '.kpi-title'
(uppercase, tracked) so it reads as a secondary section title
rather than a comparison label. */
.kpi-drivers-label { font-size: 10px; font-weight: 600; color: var(--kpi-title-color); letter-spacing: 0.08em; text-transform: uppercase; }
.kpi-drivers { display: flex; gap: 6px; flex-wrap: wrap; }
.kpi-driver { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; padding: 3px 8px; border-radius: 6px; background: #f8fafc; border: 1px solid #e5e7eb; }
.kpi-driver-dot { width: 6px; height: 6px; border-radius: 50%; }
.kpi-driver-name { font-weight: 600; color: var(--kpi-value-color); }
.kpi-driver-num { font-variant-numeric: tabular-nums; font-weight: 600; }
.kpi-narrative { background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); border-left: 3px solid var(--kpi-accent); border-radius: 6px; padding: 10px 12px; font-size: 13px; line-height: 1.5; color: #1e293b; }
.kpi-narrative-tag { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--kpi-accent); }
.kpi-narrative-body b { color: var(--kpi-accent); font-weight: 600; }
.kpi-status-chip { display: inline-flex; align-items: center; padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; }
/* Ring is responsive via aspect-ratio + clamp on width. */
.kpi-ring-wrap { position: relative; width: var(--kpi-ring-size); aspect-ratio: 1; margin: 8px auto 4px; }
.kpi-ring-svg { width: 100%; height: 100%; display: block; }
.kpi-ring-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 14%; text-align: center; gap: var(--kpi-ring-text-gap); }
/* Labels scale a notch with the ring so the proportions hold at both
the 220px and 360px ends of the responsive range. */
.kpi-ring-pct-label { font-size: clamp(12px, 1.4vw, 15px); font-weight: 600; letter-spacing: 0.06em; color: var(--kpi-title-color); text-transform: uppercase; }
.kpi-ring-target-label { font-size: clamp(12px, 1.3vw, 14px); color: var(--kpi-muted-color); }
/* In-ring headline gets a smaller scale than the bare-tile .kpi-value. */
.kpi-primary-goal-ring .kpi-value { font-size: var(--kpi-headline-size-ring); line-height: 1.1; }
/* Goal-centered layout: applied to .kpi-card by the goal-progress
renderer so sibling rows center under the ring. */
.kpi-card--goal-centered .kpi-title,
.kpi-card--goal-centered .kpi-subtitle { text-align: center; }
.kpi-card--goal-centered .kpi-rows { align-items: center; }
.kpi-card--goal-centered .kpi-row { justify-content: center; }
.kpi-card--goal-centered .kpi-row-drivers { align-items: center; }
.kpi-card--goal-centered .kpi-drivers { justify-content: center; }
.kpi-card--goal-centered .kpi-sublabel-text { text-align: center; }
/* Goal-progress BAR variant. Layout is:
row 1 (.kpi-goal-bar-head) : headline value (left) | "of <target>" (right)
row 2 (.kpi-goal-bar) : full-width track + tone-coloured fill
row 3 (.kpi-goal-bar-status): pill chip with "{pct}% — <status>"
The chip's background + foreground colours are tinted off the band
tone by the renderer (no per-tone CSS class needed). */
.kpi-goal-bar-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.kpi-goal-bar-head .kpi-ring-target-label { margin-top: 0; }
.kpi-goal-bar { width: 100%; height: 10px; border-radius: 999px; background: var(--kpi-ring-track); overflow: hidden; }
.kpi-goal-bar .fill { height: 100%; width: 0%; transition: width 0.4s ease; }
.kpi-goal-bar-status { align-self: flex-start; display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
.kpi-goal-bar-status:empty { display: none; }
/* Trend canvas mount — display flips from none to block on the
.kpi-card--has-trend reveal rule above. */
.kpi-trend { width: 100%; height: 70px; margin-top: 4px; }
/* Forecast trend: dash the projected line. The renderer pins the
projected series to the accent color with a '99' alpha suffix
(~60% opacity); we target paths with that opacity to apply the
dash pattern. Stroke colour matching in SVG attribute selectors
is exact, so the renderer's color string must match this prefix.
The two-line attribute selector form ('stroke$="99"') matches any
accent color when the projected variant is built by appending the
alpha suffix. */
.kpi-trend svg path[stroke$="99"] { stroke-dasharray: 4 3; }
.kpi-pace { position: absolute; top: 14px; right: 14px; background: var(--kpi-card-bg); border: 1px solid var(--kpi-card-border); border-radius: 8px; padding: 6px 10px; text-align: right; box-shadow: 0 1px 2px rgba(0,0,0,0.03); }
.kpi-pace-label { font-size: 11px; font-weight: 600; color: var(--kpi-title-color); letter-spacing: 0.05em; }
.kpi-pace-value { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 2px; }
HTML
The shared card scaffold inside the #chart mount — the superset of slots every card can fill. Unused slots render as nothing, so the same HTML serves all six examples.
Preview
<div id="chart">
<div class="kpi-card" role="figure" aria-labelledby="kpi-title">
<div class="kpi-title" id="kpi-title"></div>
<div class="kpi-subtitle"></div>
<!-- PRIMARY-ROW: empty container; renderer clones one of the
per-kind <template>s below into here for each primary cell.
A single primary fills one cell (full width); an array
primary fills N cells side-by-side. -->
<div class="kpi-primary-row"></div>
<!-- Per-kind primary templates. The renderer clones the matching
template per spec.layers[0].kpi.primary cell and appends the
clone into .kpi-primary-row. Templates carry no per-card data;
they only define the slot DOM each kind needs. -->
<template class="kpi-primary-tmpl-headline">
<div class="kpi-primary kpi-primary-headline">
<div class="kpi-primary-label"></div>
<div class="kpi-value"></div>
<div class="kpi-pace" style="display:none;">
<div class="kpi-pace-label"></div>
<div class="kpi-pace-value"></div>
</div>
</div>
</template>
<template class="kpi-primary-tmpl-goal-ring">
<div class="kpi-primary kpi-primary-goal-ring">
<div class="kpi-primary-label"></div>
<div class="kpi-ring-wrap">
<svg class="kpi-ring-svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" fill="none" stroke="#f1f5f9" stroke-width="8"/>
<circle class="kpi-ring-progress" cx="50" cy="50" r="42" fill="none" stroke-width="8" stroke-linecap="round" transform="rotate(-90 50 50)"/>
</svg>
<div class="kpi-ring-center">
<div class="kpi-ring-pct-label"></div>
<div class="kpi-value"></div>
<div class="kpi-ring-target-label"></div>
</div>
</div>
</div>
</template>
<template class="kpi-primary-tmpl-goal-bar">
<div class="kpi-primary kpi-primary-goal-bar">
<div class="kpi-primary-label"></div>
<div class="kpi-goal-bar-head">
<div class="kpi-value"></div>
<div class="kpi-ring-target-label"></div>
</div>
<div class="kpi-goal-bar"><div class="fill"></div></div>
<div class="kpi-goal-bar-status"></div>
</div>
</template>
<!-- ROWS: one container per row, in spec order -->
<div class="kpi-rows">
<!-- delta row -->
<div class="kpi-row kpi-row-delta">
<span class="kpi-delta"><span class="kpi-delta-arrow"></span><span class="kpi-delta-text"></span></span>
<span class="kpi-sublabel"></span>
</div>
<!-- multi-delta-strip row -->
<div class="kpi-row kpi-row-multi-delta-strip">
<div class="kpi-strip"></div>
</div>
<!-- drivers row: section label + chip strip -->
<div class="kpi-row kpi-row-drivers">
<div class="kpi-drivers-label"></div>
<div class="kpi-drivers"></div>
</div>
<!-- narrative row -->
<div class="kpi-row kpi-row-narrative">
<div class="kpi-narrative"><div class="kpi-narrative-body"></div></div>
</div>
<!-- sublabel row -->
<div class="kpi-row kpi-row-sublabel">
<div class="kpi-sublabel-text"></div>
</div>
<!-- status-chip row -->
<div class="kpi-row kpi-row-status-chip">
<span class="kpi-status-chip"></span>
</div>
</div>
<!-- TREND mount -->
<div class="kpi-trend" id="kpi-trend"></div>
</div>
</div>
Dataset (CSV)
Synthetic SaaS metrics for the cards above — daily grain, Jan–Jun 2026, three product lines. Load it into a Muze Studio Worksheet and create an Answer with it.