Scrollytelling With ApexCharts Storyboard
Storyboard, new in ApexCharts 6.0, drives a chart from the reader's scroll position. You write prose in sections called beats, pair each beat with a saved chart view, and Storyboard applies that view as the beat scrolls into the viewport. Scroll back up and it reverses. A beat can also morph the chart into a different type in one animated transition. This is the technique news outlets use for data stories, now built into the chart library instead of hand-wired with scroll listeners.
This post shows how to build one, the one design rule that separates a smooth story from a janky one, and how to drive it from code.
Key takeaways
- Storyboard pairs beats (prose elements) with views (saved chart states). Scrolling a beat past the trigger line applies its view; scrolling back reverses it.
- It is opt-in and tree-shakeable:
import 'apexcharts/features/storyboard', which pulls in Perspectives because each view is a Perspective. - The design rule: keep the same marks across beats and move emphasis by color and annotation, not by cropping or re-binning. That is what lets each transition tween element-for-element instead of popping marks in and out.
- A beat can morph the chart type (bar to donut, for example) in one transition when you add the
morphfeature. - It is driven by IntersectionObserver, not scroll listeners, and honors
prefers-reduced-motionby cutting instead of animating.
See it live
Scroll the beats on the left (or use the dots). The same twelve monthly columns stay on screen the whole way: the story recolors them, annotates two turning points, regroups them into quarters, and finally curls them into a donut. Scrolling back up rewinds every step.
One year of revenue
341 k$ across twelve months. A slow start and a strong finish, with two turning points hiding in plain sight.
March: the outage
A bad deploy took sign-ups offline for two days. March is the only month that broke the climb. The spotlight narrows.
May: v2 ships
The hinge of the year. From May on, every month tops the last, and the slope never looks back.
The quarters take shape
Same columns, new grouping. Each quarter beats the last by half again or more: 35 k$ in Q1 grows to 157 k$ by Q4.
The year in one circle
The twelve months curl into a ring, each column its own slice. The quarter colors survive the change of shape.
Scroll the story on the left, or use the beat dots. Each beat is a saved view; scrolling back up reverses it.
What is a beat?
A beat is the unit of a Storyboard. It has three parts, all optional except a way to identify its trigger element:
| Field | What it does |
|---|---|
selector (or el) | The prose element whose scroll position triggers the beat |
view | The saved chart state to apply (a Perspective): zoom window, hidden series, theme |
options | An updateOptions payload merged into the same render, so the beat can restyle or change chart.type |
announce | Text pushed to the chart's aria-live region for screen readers |
When a beat's element crosses the viewport trigger line (the middle by default), Storyboard applies that beat's view and merges its options. Because it uses IntersectionObserver rather than a scroll handler, there is no per-pixel work on the main thread.
Enable the feature and bind beats
Import the feature as a side effect, then call bind() after the chart renders. Storyboard includes Perspectives automatically, so you do not import it separately.
import ApexCharts from 'apexcharts'
import 'apexcharts/features/storyboard' // includes Perspectives
import 'apexcharts/features/morph' // only needed for the type-change beat
const chart = new ApexCharts(document.querySelector('#chart'), {
series: [{ name: 'Revenue (k$)', data: [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61] }],
chart: {
type: 'bar',
animations: { chartTypeMorph: { enabled: true, speed: 650 } },
},
plotOptions: { bar: { distributed: true } },
xaxis: { categories: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] },
})
await chart.render()
chart.storyboard.bind({
beats: [
{ selector: '[data-beat="0"]', options: { colors: allIndigo } },
{ selector: '[data-beat="1"]', options: { colors: spotlightMarch, annotations: marchOutage } },
{ selector: '[data-beat="2"]', options: { colors: fromMayOn, annotations: v2Ships } },
{ selector: '[data-beat="3"]', options: { colors: byQuarter } },
{ selector: '[data-beat="4"]', options: donutPayload }, // morphs bar -> donut
],
})
Your prose elements just need matching selectors:
<section data-beat="0"><h3>One year of revenue</h3><p>...</p></section>
<section data-beat="1"><h3>March: the outage</h3><p>...</p></section>
<!-- ...one per beat... -->
The one rule that keeps it smooth
The difference between a story that flows and one that flickers is whether the marks stay put. Here is the rule:
Keep the same marks across every beat. Move emphasis with color and annotations, never by cropping or re-binning the data.
In the live demo, all five beats render the same twelve columns with the same twelve values. The March beat does not filter down to one bar; it recolors eleven bars to a faded grey and one to red. The quarters beat does not regroup the data into four bars; it recolors the same twelve into four color groups. Because the mark count and their values never change, ApexCharts tweens each column from its old color and height to its new one. Nothing enters or leaves, so nothing pops.
Contrast the two approaches:
| Approach | What the reader sees |
|---|---|
| Wrong: each beat sets a different, filtered dataset | Bars appear and disappear on every scroll step; the chart feels like five different charts flickering past |
| Right: every beat keeps the same marks, changing only color and annotations | One chart that continuously restyles itself; every transition is a smooth tween |
The single exception is a deliberate shape change, which brings us to morphing.
Morphing the chart type in a beat
The finale in the demo changes chart.type from bar to donut. Because the morph feature is imported and chartTypeMorph is enabled, this is not a teardown: each of the twelve columns curls into its own donut slice in one animated transition. The trick that makes it a clean one-to-one morph is that the donut has exactly twelve slices for the twelve columns, and the quarter colors carry over, so the reader can track which month went where.
const donutPayload = {
chart: { type: 'donut' },
series: [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61], // same twelve values
labels: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
colors: byQuarter,
}
Morphing falls back to an instant snap when the source and target types (or their data shapes) are incompatible, so it never throws; it just stops animating.
Driving it from code
bind() returns and wires up scroll, but you can also drive the story programmatically. The demo's dots and Prev/Next buttons use these:
chart.storyboard.goTo(2) // jump to a beat by index or key
chart.storyboard.current() // { index, key } of the active beat, or null
chart.storyboard.unbind() // detach all beats and observers
chart.addEventListener('beatChange', (c, info) => {
// info: { index, key, el, direction }
highlightStep(info.index)
})
beatChange fires on every activation, whether from scrolling or from goTo, so it is the right place to sync page UI like a step highlight or a progress chip.
When to use Storyboard (and when not to)
Storyboard is for explanatory, narrative pages where the chart should reveal itself as the reader moves through prose:
- A quarterly report walkthrough
- A data-journalism story
- A product onboarding tour that explains a metric step by step
It is the wrong tool for a chart the reader simply looks at or explores freely. For a dashboard, use Linked Views instead. For a view you want to capture and share by URL rather than by scroll, use Perspectives directly. For an ordinary interactive chart, you need none of this.
Common mistakes
- Changing the dataset per beat. This is the number-one cause of janky stories. Keep the marks; change color and annotations.
- Forgetting that
updateOptionsmerges. Each beat should carry the full state it depends on. If beat 2 adds an annotation and beat 3 does not clear it, the annotation lingers. Clear it explicitly (annotations: { xaxis: [] }). - Binding before render. Call
chart.storyboard.bind()afterchart.render()resolves, so the chart and its DOM exist. - Expecting morph between incompatible types. Cross-type morphs work for bar to pie/donut/radialBar/polarArea and the trivial pie/donut/polarArea cases; anything else snaps.
Summary
ApexCharts Storyboard turns scroll position into chart choreography. You pair prose beats with saved views, and the chart restyles, annotates, and morphs as the reader scrolls, reversing cleanly on the way back. Import apexcharts/features/storyboard, call chart.storyboard.bind({ beats }) after render, and follow the one rule that keeps it smooth: keep the same marks across beats and move emphasis with color and annotation. For the full option and method reference, see the Storyboard guide; for the feature it builds on, see Perspectives. This is one of several authoring and narrative features in ApexCharts 6.0.
Frequently asked questions
What is scrollytelling in ApexCharts?
Scrollytelling is a narrative technique where scrolling drives a visual. In ApexCharts 6.0 the Storyboard feature implements it: you pair prose sections (beats) with saved chart views, and as each beat crosses the viewport trigger line the chart applies that beat's view. Scrolling back up reverses the change. It uses IntersectionObserver, not scroll listeners, so it stays smooth.
How do I enable Storyboard in ApexCharts?
Import the feature as a side effect (import 'apexcharts/features/storyboard'), then call chart.storyboard.bind({ beats }) after render. Each beat has a selector for the prose element that triggers it, an optional view to apply, and an optional options payload merged via updateOptions. Storyboard includes Perspectives automatically, because each beat's view is a Perspective.
Can a Storyboard beat change the chart type?
Yes. A beat can carry an options payload that is merged into the same render as its view, so it can restyle the chart or change chart.type. When you also import the morph feature and enable animations.chartTypeMorph, a type change (for example bar to donut) tweens shape to shape instead of snapping, with each column curling into its own slice.
Does Storyboard work backward when scrolling up?
Yes. Beats are reversible: scrolling back up re-applies the previous beat's view. The key design rule is that each beat should carry the full state it depends on (colors, annotations, type), because updateOptions merges. When every beat fully specifies its own state, forward and backward scrolling both land on exactly the right view.