More Scroll Snippets
Scrollytelling Chart — Free HTML CSS JS Snippet
Scrollytelling Chart · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scrollytelling Chart — A Sticky Chart Narrated by Scroll Position

Scrollytelling is the data-journalism pattern popularized by the NYT and The Pudding: narrative steps scroll up one column while a sticky chart beside them morphs to illustrate each step. The reader controls the pace; the chart always shows the data for the paragraph they're reading. This snippet builds the full scrollama-style setup — sticky graphic, step detection, dataset morphing — in vanilla HTML, CSS, and an IntersectionObserver, with no charting or animation library.
The chart is data-driven from a steps array
All content lives in a STEPS array: each entry holds a title, five values, and which bar leads. The bars are built once from the first row; advancing a step only rewrites heights, labels, and a lead class. That separation — build once, restyle per step — is what makes the morph feel like *one chart changing* rather than charts being swapped, and it means adding a step is adding a data row, not markup.
CSS transitions are the animation engine
applyStep just writes height: NN% inline; the 0.8s cubic-bezier(.22,1,.36,1) transition on .ssc-bar animates every bar to its new value with a slight overshoot settle. Letting CSS interpolate means the JavaScript never runs per-frame — the observer fires four times across the whole section, and the browser tweens the rest on the compositor-adjacent fast path for height-in-a-flex-row.
Step detection uses a middle-band observer
Each narrative step is watched with rootMargin: '-45% 0px -45% 0px', collapsing the detection zone to a 10%-tall band across the viewport's center. A step triggers exactly when its text is where the reader's eyes are, and only one step can occupy the band at a time — the scrollama library's core trick, in six lines. Scrolling upward re-fires earlier steps automatically, so the story is fully bidirectional.
position: sticky keeps the graphic in view
The chart panel is position: sticky; top: 0; height: 100vh inside a column as tall as all four steps, so it pins while steps pass and releases at the section's edges with no JavaScript pinning and none of the jump artifacts pinning libraries can introduce.
The lead-bar highlight carries the narrative
Each dataset marks a lead index; that bar swaps to a cyan gradient via the is-lead class. Color is doing narrative work here — in step four, the highlight physically jumps from West to East at the same moment the text announces the overtake, which is the kind of text/graphic synchronization scrollytelling exists for. Value labels use tabular-nums so they don't jitter as digits change.
Steps dim rather than hide
Inactive steps sit at 30% opacity, keeping the story's shape visible while focusing the active paragraph — and on mobile the sticky column hides entirely, letting steps read as a plain article (add per-step inline charts there if needed).
Customizing it
Add datasets (bars rebuild from the first row's length), restyle bars into columns, dots, or lines, or drive any DOM-based graphic — maps, diagrams, counters — from applyStep. Related: scroll story steps with a pinned panel, the bar chart base component, sticky scroll features for the marketing cousin, and a scroll year timeline for date-driven stories.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the scrollama-style step detection by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the bars are built once from the first dataset row and only ever have their height rewritten afterward, or why the rootMargin band collapsed to the vertical middle guarantees exactly one narrative step is ever active. The same assistant can help optimize it — asking whether the 0.8s cubic-bezier bar transition should be shortened for datasets with many more bars, or whether the STEPS array's lead index could instead be derived automatically from whichever value is highest each step. It's also useful for extending the effect: ask it to add a second chart type (a line chart) driven by the same applyStep function, annotate bars with change indicators like up/down arrows between steps, or sync a map visualization alongside the bar chart from the same observer. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "scrollytelling chart" in plain HTML, CSS, and JavaScript using only CSS position: sticky, native IntersectionObserver, and CSS transitions — no charting library, no animation library, no scroll event listener.
Requirements:
- A two-column layout: a left column of narrative step blocks (each carrying a data-step index and containing a heading and paragraph describing that moment in the story), and a right column containing a sticky panel (position: sticky, top: 0, height: 100vh) with a title, a bar chart container, and axis labels.
- Store all the narrative data in a single JavaScript array, one entry per step, each with a title, an array of numeric values (one per bar, on a shared 0-100 scale), and an index marking which bar should be visually highlighted as the "lead."
- Build the bar elements only once, using the first step's values to determine how many bars to create — do not rebuild or replace bar elements when the step changes.
- Write an applyStep(index) function that updates the panel title text, sets each bar's height as an inline percentage style from that step's values, updates each bar's numeric label, and toggles a "lead" class on whichever bar matches that step's lead index while removing it from all others.
- Give the bar elements a CSS transition on the height property (a slower duration like 0.8s with an eased overshoot curve) so that whenever applyStep changes a height value, the browser animates smoothly between the old and new height with no JavaScript-driven animation loop.
- Create a single IntersectionObserver watching every narrative step block, using a rootMargin with large negative top and bottom percentages so its effective detection area is a thin horizontal band across the vertical middle of the viewport, and call applyStep with the intersecting step's index inside the callback.
- Confirm scrolling back up through the steps re-triggers earlier steps' applyStep calls and animates the bars back to their earlier values, so the story is fully reversible with no separate rewind logic.Step by step
How to Use
- 1Paste HTML, CSS, and JSNo CDNs — sticky CSS and an observer do everything.
- 2Scroll into the storyThe chart pins while step one's text is centered.
- 3Keep scrollingEach step morphs the bars to its dataset.
- 4Watch the lead barThe cyan highlight jumps as the story turns.
- 5Scroll back upEarlier steps re-fire — the story rewinds.
- 6Edit the STEPS arrayTitles, values, and lead index drive everything.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
An IntersectionObserver watches every step with rootMargin: '-45% 0px -45% 0px', shrinking detection to a thin band across the viewport's middle. A step intersects only while its text occupies that band — where the reader is actually looking — and applyStep fires with its index. It's the core trick of the scrollama library, implemented in a few lines.
applyStep writes each bar's height as an inline percentage, and a CSS transition — 0.8s with a cubic-bezier(.22,1,.36,1) overshoot — interpolates from the old value. JavaScript runs only at step boundaries (four times total), not per frame; the browser does all tweening. The bars are plain flex-column divs, so no canvas or SVG library is involved.
Because the morph is the message: seeing East's bar physically grow past West communicates "overtake" in a way a hard swap can't. Building bars once from the first dataset and only rewriting heights, labels, and the lead class guarantees visual continuity and keeps DOM churn at zero during the story.
Add rows to the STEPS array and matching .ssc-step blocks with incremented data-step indexes — bars, labels, and highlights all derive from the data. Values are percentages of the chart height, so normalize your real numbers to 0–100 (or compute the max and scale). applyStep can drive any DOM graphic, not just bars: maps, counters, or diagrams.
Keep the sticky layout in CSS (Tailwind: sticky top-0 h-screen) and hold the active step index in state, set from an IntersectionObserver registered in a mount effect — useEffect, onMounted, or ngAfterViewInit — and disconnected in the cleanup. Render bars from the active dataset and let the same CSS transitions animate between renders; heights as inline styles diff cleanly in all three frameworks.