Detent web components
By Shane Logsdon

Charts & metrics

Stat Card

Implemented

A KPI, its change and a sparkline in one tile

detent-stat-card

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-data
Group
Charts & metrics

8.7 KB gzip for the class module

Variants

A StatGrid of three tiles (revenue, churn with good-when="down", latency) and one tile with no trend data. Tab into a tile and press ArrowRight / ArrowLeft to scrub the sparkline, Escape to clear. Numbers are local demo data: nothing is fetched and no money is moved.

Ready.

Recorded states

  • reduced motion recorded state; no setter
  • active recorded state; no setter

Usage

Import

import '/components/stat-card/define.js';

// or register it yourself
import { DetentStatCard } from '/components/stat-card/stat-card.js';

Attributes

  • animated
  • color
  • delta
  • good-when
  • label
  • period
  • previous
  • value

Properties

  • format
  • trend
  • trendLabels

Slots

  • default
  • stat-grid.children

Events

No events are recorded for this component.

Example

Runnable example: site/examples/stat-card.ts. The snippet below is generated from the catalogued attributes.

<detent-stat-card
  animated
  color
  delta
  good-when="up"
  label
  period="vs previous period"
  previous
  value
></detent-stat-card>

Property-only values

const element = document.querySelector('detent-stat-card');
if (element !== null) {
  // trend: number[]
  element.trend = [];
  // trendLabels: string[]
  element.trendLabels = [];
  // format: (n: number) => string
  element.format = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/StatCard.tsx sha256 281b6647295b…

Native files

The native module and its artifacts:

  • src/components/stat-card/stat-card.ts
  • src/components/stat-card/styles.css
  • src/components/stat-card/define.ts
  • docs/components/stat-card.md
  • site/examples/stat-card.ts
  • tests/browser/stat-card.spec.ts
  • evidence/components/stat-card.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
animatedanimatedbooleantruePresence semantics: present means true, absent means false.
colorcolorstringCHART_COLORS[0]Kebab-case attribute; camelCase property.
deltadeltanumber—Kebab-case attribute; camelCase property.
good-whengoodWhen"up" | "down""up"Kebab-case attribute; camelCase property.
labellabelstring—Kebab-case attribute; camelCase property.
periodperiodstring"vs previous period"Kebab-case attribute; camelCase property.
previouspreviousnumber—Kebab-case attribute; camelCase property.
valuevaluenumber—Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
format(n: number) => stringIntl.NumberFormatProperty-only: assign from script; not accepted as an attribute.
trendnumber[]—Property-only: assign from script; not accepted as an attribute.
trendLabelsstring[]—Property-only: assign from script; not accepted as an attribute.

Events

No custom events are recorded for this component.

Slots

Slots
SlotNotes
defaultConsumer content; rendered through a slot, never re-created.
stat-grid.childrenConsumer content; rendered through a slot, never re-created.

CSS customisation

  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • Controlled/uncontrolled React pair maps to initial-value attributes plus owned state and change events.
  • StatGrid ships as the auto-fitting [part="grid"] of the same tag (PC-2.1 keeps one tag per row): extra tiles are assigned to the stat-grid.children slot instead of being React children of a second component.
  • The row's flattened stat-grid.class-name pseudo-attribute is not implemented: a React className prop cannot be an HTML attribute and a dotted name is not kebab-case (PC-2.3). Grid styling goes through [part="grid"] and --detent-stat-card-radius.
  • Upstream's tweens keep their timing: the count-up is a rAF tween on EASE_OUT cubic-bezier(0.23, 1, 0.32, 1) (1.1 s intro, 0.22 s after); on the first render with a line, the line draws through pathLength (1.1 s, 0.15 s delay, EASE_OUT) and the area fades in (0.8 s, 0.5 s delay, ease-in-out) through WAAPI, once, as upstream's initial; the latest-point pulse is a CSS keyframe (scale 1 to 2.8, opacity 0.4 to 0, 1.8 s ease-out). Reduced motion removes the pulse, the draw and the fade and makes the count-up immediate.
  • The scrub cursor moves between points on upstream's spring (600/44/0.5) through the shared helper; its first point of a scrub appears in place (upstream jump()), and reduced motion moves it at once.
  • The tile is themed from --detent-color-* tokens (upstream hard-codes neutral shades), so light and dark are both supported (PC-9.5).
  • Long labels are ellipsised with a title-free truncation; upstream uses Tailwind's truncate with no accessible alternative, so the full label remains in the card's aria-label here.

What it does

A KPI tile with a label, a count-up value, a delta pill, a period line and a scrubbable sparkline, plus an auto-fitting grid for several tiles.

Upstream behaviour
  • Count-up headline number on mount that follows the sparkline while scrubbing
  • Delta pill computes change from a previous value or accepts an explicit percentage
  • Direction-aware coloring with goodWhen="down" for churn, latency, and costs
  • Hand-written SVG sparkline with a smooth monotone curve and gradient fill
  • Pointer and touch scrubbing with a spring cursor and per-point labels
  • Keyboard support with ArrowLeft, ArrowRight, and Escape
  • StatGrid auto-fits tiles from a single mobile column to full-width rows
  • Light and dark themes plus prefers-reduced-motion support
Differences from upstream
  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • Controlled/uncontrolled React pair maps to initial-value attributes plus owned state and change events.
  • StatGrid ships as the auto-fitting [part="grid"] of the same tag (PC-2.1 keeps one tag per row): extra tiles are assigned to the stat-grid.children slot instead of being React children of a second component.
  • The row's flattened stat-grid.class-name pseudo-attribute is not implemented: a React className prop cannot be an HTML attribute and a dotted name is not kebab-case (PC-2.3). Grid styling goes through [part="grid"] and --detent-stat-card-radius.
  • Upstream's tweens keep their timing: the count-up is a rAF tween on EASE_OUT cubic-bezier(0.23, 1, 0.32, 1) (1.1 s intro, 0.22 s after); on the first render with a line, the line draws through pathLength (1.1 s, 0.15 s delay, EASE_OUT) and the area fades in (0.8 s, 0.5 s delay, ease-in-out) through WAAPI, once, as upstream's initial; the latest-point pulse is a CSS keyframe (scale 1 to 2.8, opacity 0.4 to 0, 1.8 s ease-out). Reduced motion removes the pulse, the draw and the fade and makes the count-up immediate.
  • The scrub cursor moves between points on upstream's spring (600/44/0.5) through the shared helper; its first point of a scrub appears in place (upstream jump()), and reduced motion moves it at once.
  • The tile is themed from --detent-color-* tokens (upstream hard-codes neutral shades), so light and dark are both supported (PC-9.5).
  • Long labels are ellipsised with a title-free truncation; upstream uses Tailwind's truncate with no accessible alternative, so the full label remains in the card's aria-label here.
Previous: Stacked Cards Next: Stepper

Docs

Components

Project