Callout Stat Hero
A hero layout built around one giant number filling the first screen, with SVG leader lines drawn one by one as you scroll, connecting parts of the number to surrounding annotation cards that surface in sequence.
npx shadcn@latest add https://webberui.com/r/callout-stat-hero.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CalloutStatHero />
Installation
npx shadcn@latest add https://webberui.com/r/callout-stat-hero.jsonOr, once registries are configured in components.json, install it as @webberui/callout-stat-hero.
Usage
import { CalloutStatHero } from "@/components/ui/callout-stat-hero";
<CalloutStatHero
eyebrow="2025 customer satisfaction"
value="94%"
caption="Share of surveyed customers who would recommend us"
annotations={[
{ x: 8, y: 34, side: "left", title: "5 quarters of growth in a row" },
{ x: 20, y: 76, side: "left", title: "12,480 responses" },
{ x: 92, y: 30, side: "right", title: "NPS 62" },
{ x: 82, y: 78, side: "right", title: "First response < 2h" },
]}
/>By default the whole window is the scroll container (an h-screen stage). If you put it inside a nested overflow-y-auto container, pass that container's ref to container and use stageClassName to override the stage height to the container's visible height:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[480px] overflow-y-auto">
<CalloutStatHero
value="94%"
annotations={annotations}
container={scrollerRef}
stageClassName="h-[480px]"
/>
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | React.ReactNode | — | The giant number that fills the first screen |
eyebrow | React.ReactNode | — | The small label above the number |
caption | React.ReactNode | — | The description text below the number |
annotations | StatAnnotation[] | — | The list of annotations surrounding the number, drawn and surfaced in sequence as you scroll |
connector | "straight" | "elbow" | "straight" | Leader line style: a straight line, or an elbow with a horizontal lead-in segment |
scrollScale | number | estimated from the annotation count | Height of the scroll track as a multiple of the pinned stage height |
offsetTop | number | 0 | Sticky top position of the pinned stage (px) |
container | RefObject<HTMLElement | null> | — | Ref of the nested scroll container; the window is the scroll container by default |
stageClassName | string | "h-screen" | Class of the pinned stage container; override it to the container's visible height when nesting inside a scroll container |
numberClassName | string | — | Overrides the class of the giant number (size, weight, color, …) |
aria-label | string | "數據主視覺" | Accessible name of the whole section — the built-in default is Traditional Chinese for "data hero", so pass this prop to localize it |
StatAnnotation
| Field | Type | Default | Description |
|---|---|---|---|
x | number | — | Horizontal position of the anchor (percentage of the number box width, 0–100) |
y | number | — | Vertical position of the anchor (percentage of the number box height, 0–100) |
title | React.ReactNode | — | Annotation card title |
description | React.ReactNode | — | Annotation card body (optional) |
side | "left" | "right" | determined automatically from x | Whether the card docks to the left or right column |
id | string | index | Custom React key |
How it works
- The section pins the whole stage with
position: sticky, with the outer track height stretched byscrollScale, so the scroll distance is the progress source for drawing the leader lines one by one. - The leader line endpoints are measured from the DOM after mount (midpoint of the card's inner edge → the number anchor), tracked through reflows with
ResizeObserver, and converted to stage coordinates for drawing, which is why the text always stays crisp and is never blurred by scaling. - The lines are drawn with
pathLength, and the small anchor circle pops in only once that line has finished drawing, while the annotation card surfaces with opacity and a slight offset, producing a "draw the line → light the point → present the card" rhythm. x/yare percentages relative to the number element's bounding box, so they can point precisely at a particular character or corner; leave margin for anchors near0or100so they are not clipped by theoverflow-hiddenstage.
Accessibility
- With "reduce motion" enabled at the system level, it renders a static summary instead, with no scrolling and no leader lines: the giant number plus a numbered list of the annotations, complete and readable.
- The leader line
svg, the anchors, and the small start circles are all markedaria-hiddenso they do not interfere with assistive technology; the number and the annotation text are announced normally. - The stage container carries
role="group"and anaria-label, so it can be navigated as a single identifiable section.
Drilldown Stat Grid
A grid of KPI tiles; clicking a tile expands a full-width detail panel below its row while the remaining tiles move down to make room, and clicking again collapses it.
Weighted Metric Wall
A wall of KPI tiles whose sizes are computed automatically from the weight of each value, revealed largest-first in a cascade, expanding a detail layer on hover.