Sticky Scroll — <mv-sticky-scroll>
Scrollytelling section: steps hang off a progress rail that fills continuously with scroll (done / active / upcoming nodes), while the pinned panel wipes between visuals along a single direction-aware seam and a segmented meter tracks the current step; stacks on mobile.
Category Scroll Type Web Component (<mv-sticky-scroll>) Status stable Keywords sticky, scrollytelling, features, steps, timeline, progress, wipe, feature-section
When to use
A feature walkthrough pairs text steps with a pinned visual that wipes to the next one as the reader scrolls Scrollytelling needs visible progress, with a filling rail, step states and a segmented step meter A product tour should read step by step on desktop and stack cleanly on mobile
Avoid when
Users should jump between sections freely instead of scrolling through them in order → use Tabs instead Steps are a multi-step form or wizard the user must complete → use Stepper instead
Get Sticky Scroll. Included in every plan: the Pro pack ships all 226 components with the CLI and the MCP server used below.
See plans
Install
node scripts/add.mjs sticky-scroll --out ./src/marvelous
AI agent with the Marvelous UI MCP server: install_components({ slugs: ["sticky-scroll"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/sticky-scroll/sticky-scroll.js, components/sticky-scroll/sticky-scroll.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="mv-ss-demo" class="mv-scroll-area" data-orientation="vertical" tabindex="0" aria-label="Sticky scroll demo">
<style>
#mv-ss-demo { width: min(100%, 56rem); height: 440px; margin-inline: auto; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg); }
#mv-ss-demo .intro { padding: 2rem 1.75rem .5rem; }
#mv-ss-demo .eyebrow { margin: 0; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--mv-accent-fg); font-weight: 600; }
#mv-ss-demo .intro h2 { margin: .35rem 0 0; font: 700 1.6rem/1.15 var(--mv-font-display); letter-spacing: -.03em; }
#mv-ss-demo mv-sticky-scroll { padding: 0 1.25rem 0 .5rem; --mv-sticky-scroll-height: 19rem; --mv-sticky-scroll-step-height: 13rem; }
#mv-ss-demo .mv-sticky-scroll-step h3 { margin: 0 0 .4rem; font-size: 1.1rem; letter-spacing: -.015em; display: flex; align-items: center; gap: .6rem; }
#mv-ss-demo .mv-sticky-scroll-step h3 small { font: 600 .7rem var(--mv-font-mono); color: var(--mv-accent-fg); }
#mv-ss-demo .mv-sticky-scroll-step p { margin: 0; color: var(--mv-fg-muted); font-size: .9rem; line-height: 1.6; }
#mv-ss-demo .outro { padding: 1rem 1.75rem 3rem; color: var(--mv-fg-muted); font-size: .85rem; text-align: center; }
#mv-ss-demo .v { padding: 1.4rem; display: grid; align-content: center; gap: .6rem; background: radial-gradient(120% 90% at 20% 0%, oklch(.72 .15 var(--hue) / .28), transparent 60%), radial-gradient(90% 80% at 100% 100%, oklch(.7 .15 calc(var(--hue) + 60) / .18), transparent 60%); }
#mv-ss-demo .sheet { position: relative; padding: 1rem 1.1rem; border-radius: var(--mv-radius-lg); background: var(--mv-bg); border: 1px solid var(--mv-border); box-shadow: var(--mv-shadow-md); display: grid; gap: .55rem; }
#mv-ss-demo .bar { height: .5rem; border-radius: 99px; background: var(--mv-bg-emphasis); }
#mv-ss-demo .cursor { position: absolute; font: 600 .68rem var(--mv-font-sans); color: white; padding: .15rem .45rem; border-radius: .35rem .35rem .35rem 0; }
#mv-ss-demo .avatars { display: flex; }
#mv-ss-demo .avatars span { width: 1.7rem; height: 1.7rem; border-radius: 50%; border: 2px solid var(--mv-bg); margin-left: -.4rem; display: grid; place-items: center; font-size: .65rem; font-weight: 700; color: white; }
#mv-ss-demo .row { display: flex; align-items: center; gap: .7rem; padding: .6rem .8rem; border-radius: var(--mv-radius-md); background: var(--mv-bg); border: 1px solid var(--mv-border); font-size: .8rem; }
#mv-ss-demo .row b { font-weight: 600; }
#mv-ss-demo .row .meta { margin-left: auto; color: var(--mv-fg-subtle); font-size: .72rem; }
#mv-ss-demo .dot { width: .55rem; height: .55rem; border-radius: 50%; background: var(--mv-accent); flex: none; }
#mv-ss-demo .pill { padding: .1rem .5rem; border-radius: 99px; font-size: .68rem; font-weight: 600; background: var(--mv-accent-subtle); color: var(--mv-accent-fg); }
#mv-ss-demo .tog { margin-left: auto; width: 2rem; height: 1.15rem; border-radius: 99px; background: var(--mv-bg-emphasis); position: relative; flex: none; }
#mv-ss-demo .tog::after { content: ""; position: absolute; top: .15rem; left: .15rem; width: .85rem; height: .85rem; border-radius: 50%; background: white; box-shadow: var(--mv-shadow-xs); }
#mv-ss-demo .tog.on { background: var(--mv-accent); }
#mv-ss-demo .tog.on::after { left: 1rem; }
#mv-ss-demo .term { border-radius: var(--mv-radius-lg); background: oklch(.18 .01 270); color: oklch(.85 .01 270); font: 500 .74rem/1.75 var(--mv-font-mono); padding: .9rem 1rem; box-shadow: var(--mv-shadow-lg); }
#mv-ss-demo .term i { font-style: normal; color: oklch(.75 .15 150); }
#mv-ss-demo .term u { text-decoration: none; color: oklch(.7 .02 270); }
</style>
<div class="intro">
<p class="eyebrow">Platform</p>
<h2>Four reasons to switch tools</h2>
</div>
<mv-sticky-scroll>
<div class="mv-sticky-scroll-steps">
<section class="mv-sticky-scroll-step">
<h3><small>01</small>Multiplayer editing</h3>
<p>Your whole team works in the same document, in real time. Everyone’s cursor stays visible and conflicts disappear.</p>
</section>
<section class="mv-sticky-scroll-step">
<h3><small>02</small>Full history</h3>
<p>Every change is recorded. Compare two versions, see who changed what and roll back in one click.</p>
</section>
<section class="mv-sticky-scroll-step">
<h3><small>03</small>Granular access</h3>
<p>Precise roles per workspace: view, comment, edit. External guests only see what you share.</p>
</section>
<section class="mv-sticky-scroll-step">
<h3><small>04</small>Instant deploys</h3>
<p>Publish straight from the editor. Preview, tests and deploy run back to back without leaving the page.</p>
</section>
</div>
<div class="mv-sticky-scroll-panel">
<div class="mv-sticky-scroll-visual v" style="--hue:280">
<div class="avatars" style="margin-left:.4rem"><span style="background:oklch(.62 .18 20)">LI</span><span style="background:oklch(.6 .15 200)">DK</span><span style="background:oklch(.62 .16 140)">SR</span></div>
<div class="sheet">
<div class="bar" style="width:55%;height:.7rem;background:var(--mv-fg)"></div>
<div class="bar" style="width:92%"></div>
<div class="bar" style="width:84%"></div>
<div class="bar" style="width:70%"></div>
<div class="bar" style="width:88%"></div>
<div class="bar" style="width:40%"></div>
<span class="cursor" style="top:2.1rem;left:62%;background:oklch(.62 .18 20)">Lina</span>
<span class="cursor" style="top:4.4rem;left:30%;background:oklch(.6 .15 200)">Daniel</span>
</div>
</div>
<div class="mv-sticky-scroll-visual v" style="--hue:220">
<div class="row"><span class="dot"></span><b>Version 12</b><span class="pill">Current</span><span class="meta">2 min ago</span></div>
<div class="row"><span class="dot" style="background:var(--mv-border-strong)"></span>Version 11 · Title changed<span class="meta">10:42 AM</span></div>
<div class="row"><span class="dot" style="background:var(--mv-border-strong)"></span>Version 10 · Section added<span class="meta">Yesterday</span></div>
<div class="row"><span class="dot" style="background:var(--mv-border-strong)"></span>Version 9 · Review pass<span class="meta">Monday</span></div>
</div>
<div class="mv-sticky-scroll-visual v" style="--hue:160">
<div class="row"><b>Product team</b><span class="pill">Edit</span><span class="tog on"></span></div>
<div class="row"><b>Leadership</b><span class="pill">Comment</span><span class="tog on"></span></div>
<div class="row"><b>External agency</b><span class="pill">View</span><span class="tog"></span></div>
<div class="row"><b>Public link</b><span class="meta" style="margin-left:0">off</span><span class="tog"></span></div>
</div>
<div class="mv-sticky-scroll-visual v" style="--hue:30">
<div class="term">
<div><u>$</u> deploy --production</div>
<div><i>✓</i> Preview built <u>(1.2s)</u></div>
<div><i>✓</i> 128 tests passed <u>(6.8s)</u></div>
<div><i>✓</i> Deployed to 12 regions <u>(5.9s)</u></div>
<div style="margin-top:.4rem;color:white">Live in 14s → acme.dev</div>
</div>
</div>
</div>
</mv-sticky-scroll>
<p class="outro">The panel follows whichever step crosses the middle of the container.</p>
</div>
API
Attributes
Name Type Default Description focusnumber 0–1 0.45Position of the reading line within the container: the step whose top has crossed it becomes active. scrollerselector | window Reference scroll container. Default: the nearest scroll container, or the page. no-meterboolean Hides the segmented step meter overlaid on the panel. data-directionforward | backward Set by the component: direction of the last step change (for styling).
Properties
Name Type Description indexnumber Index of the active step (read-only).
Events
Name Description mv-changeNew active step. detail: { index, previous }.
Content structure
Name Description .mv-sticky-scroll-steps > .mv-sticky-scroll-stepText blocks (left column); each gets data-state = done | active | upcoming. .mv-sticky-scroll-panel > .mv-sticky-scroll-visualVisuals (right, sticky column), matched to steps by order; earlier ones get data-before.
CSS classes
Name Description mv-sticky-scroll-meter / -meter-bar / -meter-countGenerated, aria-hidden step meter inside the panel.
CSS variables
Name Default Description --mv-sticky-scroll-accentvar(--mv-accent)Rail fill, nodes and meter color. --mv-sticky-scroll-topvar(--mv-space-6)Distance from the panel to the top of the container. --mv-sticky-scroll-height20remPanel height. --mv-sticky-scroll-height-sm11remPanel height in the stacked layout (< 36rem). --mv-sticky-scroll-step-heightautoMinimum step height (scroll rhythm). --mv-sticky-scroll-gap / -step-gap / -tailSpacing: columns, steps, space after the last step. --mv-sticky-scroll-indexActive index, written by JS (read-only). --mv-sticky-scroll-progressOverall progress 0–1 through the steps, written by JS (read-only).
Accessibility
The active step gets aria-current="step"; inactive visuals are inert and aria-hidden, so screen readers only read the current visual. The rail, nodes and meter are decorative (pseudo-elements / aria-hidden). Keyboard focus inside a step activates its visual. Reduced motion: the wipe and parallax become instant; the rail still tracks progress. Before the element is defined, all text and the first visual are visible.