Scrollytelling Stage
A two-column scroll narrative with a sticky data stage on the left and step-by-step prose on the right; the stage's chart state, highlight band, and annotation line morph between steps by tweening on the step index.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/scrollytelling-stage.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ScrollytellingStage />
Installation
npx shadcn@latest add "https://webberui.com/r/scrollytelling-stage.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/scrollytelling-stage.
Usage
Each ScrollytellingStep uses scene to describe the state the stage on the left should show when you scroll to that step; as you scroll, the stage tweens and morphs between the scenes of adjacent steps.
import {
ScrollytellingStage,
ScrollytellingStep,
} from "@/components/ui/scrollytelling-stage";
<ScrollytellingStage>
<ScrollytellingStep title="A steady start" scene={{ data: [30, 42, 38, 46, 40] }}>
<p>Baseline: weekly activity stays roughly flat.</p>
</ScrollytellingStep>
<ScrollytellingStep
title="Midweek surge"
scene={{
data: [30, 55, 74, 60, 44],
annotation: { at: 2, label: "Wednesday peak" },
}}
>
<p>After the new feature shipped, Wednesday produced a single-day peak.</p>
</ScrollytellingStep>
<ScrollytellingStep
title="Carried through"
scene={{
data: [34, 58, 70, 82, 76],
highlight: [3, 4],
annotation: { at: 3, label: "Holding at a high" },
}}
>
<p>The highlighted range shows Thursday and Friday reaching new highs.</p>
</ScrollytellingStep>
</ScrollytellingStage>;If the scrolling happens inside a fixed-height nested overflow-y-auto container, pass that container's ref to container:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
<ScrollytellingStage container={scrollerRef}>
{/* ...steps */}
</ScrollytellingStage>
</div>;When you need a fully custom stage, use renderStage instead (replacing the built-in data chart); it receives the current activeIndex and the continuous step progress (a MotionValue, from 0 to the number of steps minus 1):
<ScrollytellingStage
renderStage={({ activeIndex, progress }) => (
<MyCustomStage step={activeIndex} progress={progress} />
)}
>
{/* ...steps */}
</ScrollytellingStage>Props
ScrollytellingStage
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A set of ScrollytellingSteps |
container | RefObject<HTMLElement | null> | — | Ref of the nested overflow scroll container; when omitted, the window is the scroll container |
maxValue | number | 100 | Upper bound of the values, used to normalize the bar heights |
stickyTop | number | 16 | Pinning position of the stage on the left (px) |
renderStage | (state) => ReactNode | — | Custom stage rendering, receiving { activeIndex, progress } |
className | string | — | Class for the outer container |
stageClassName | string | — | Class for the stage frame on the left; set the accent color with text-* |
ScrollytellingStep
| Prop | Type | Default | Description |
|---|---|---|---|
scene | StageScene | — | The stage state matching this step (see below) |
title | ReactNode | — | Step title |
children | ReactNode | — | Step body copy |
className | string | — | Class for the step's outer element |
StageScene
| Field | Type | Description |
|---|---|---|
data | number[] | Value of each bar, normalized into a height by maxValue |
highlight | [number, number] | Highlight range [start index, end index] (inclusive, and may be fractional); omit it to hide the highlight |
annotation | { at: number; label: string } | The annotation line points at bar number at and shows label; omit it to hide the annotation |
How it works
useScrolltracks the scroll progress of the step column on the right and converts it into a continuous step value (from 0 to the number of steps minus 1), which is the single source driving every tween — so the bar heights, the highlight band, and the annotation line always deform in sync.- The highlight band and the annotation line use
currentColor, so atext-*onstageClassNamesets the accent color for everything at once. - Bars near the annotation line brighten to the accent color, producing a spotlight effect that travels with the scroll.
- For scenes missing a
highlightor anannotation, the matching graphic fades out smoothly, so the morph never jumps.
Accessibility
- When the user has "reduce motion" enabled at the system level, the stage switches instantly from step to step (no tween, no transition), with the layout and content completely unchanged.
- The stage on the left carries
role="img"and anaria-labeldescribing the current step; the SVG graphics inside are hidden from assistive technology. - The steps are a semantic ordered list (
<ol>/<li>) with the active step markedaria-current="step"and emphasized through opacity and its title; the keyboard simply uses the browser's native scrolling to read through them in order.
Recap Story Scroll
A full-screen, scene-by-scene scroll-snap narrative scroll — as each scene settles, oversized numbers and keywords are choreographed in, the background color transitions across the whole scene by theme, and progress dots and autoplay are built in.
Atlas Plate
A field-guide style scroll walkthrough — a full-bleed plate is pinned on one side and numbered notes run down the other; scrolling to a note pans and zooms the camera to its hotspot and lights up the numbered leader line.