Scrollytelling

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

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/scrollytelling-stage.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

100
16
Active edited usageDefault props
<ScrollytellingStage/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/scrollytelling-stage.en.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

PropTypeDefaultDescription
childrenReactNode—A set of ScrollytellingSteps
containerRefObject<HTMLElement | null>—Ref of the nested overflow scroll container; when omitted, the window is the scroll container
maxValuenumber100Upper bound of the values, used to normalize the bar heights
stickyTopnumber16Pinning position of the stage on the left (px)
renderStage(state) => ReactNode—Custom stage rendering, receiving { activeIndex, progress }
classNamestring—Class for the outer container
stageClassNamestring—Class for the stage frame on the left; set the accent color with text-*

ScrollytellingStep

PropTypeDefaultDescription
sceneStageScene—The stage state matching this step (see below)
titleReactNode—Step title
childrenReactNode—Step body copy
classNamestring—Class for the step's outer element

StageScene

FieldTypeDescription
datanumber[]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

  • useScroll tracks 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 a text-* on stageClassName sets 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 highlight or an annotation, 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 an aria-label describing 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 marked aria-current="step" and emphasized through opacity and its title; the keyboard simply uses the browser's native scrolling to read through them in order.