WebberUI

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.

How to install Pro components →See the plans →

Loading preview…
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.

100
16
<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

PropTypeDefaultDescription
childrenReactNodeA 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) => ReactNodeCustom stage rendering, receiving { activeIndex, progress }
classNamestringClass for the outer container
stageClassNamestringClass for the stage frame on the left; set the accent color with text-*

ScrollytellingStep

PropTypeDefaultDescription
sceneStageSceneThe stage state matching this step (see below)
titleReactNodeStep title
childrenReactNodeStep body copy
classNamestringClass 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.

On this page