Scroll Stack <mv-scroll-stack>
Sticky cards that stack as you scroll: each incoming card settles into place while the ones beneath recede a step (smaller, dimmer), scroll-driven where supported, focus uncovers covered cards, a plain list under reduced motion.
| Category | Scroll |
|---|---|
| Type | Web Component (<mv-scroll-stack>) |
| Status | stable |
| Keywords | stack, sticky, cards, deck, process, steps, scroll-driven, view-timeline |
When to use
- A sequence of process steps or case studies should stack as sticky cards while the page scrolls
- A marketing section should present a few large cards one after another, with earlier ones receding behind
- Cards hold links or buttons that keyboard users must be able to reach even once covered
Avoid when
- Users should swipe or drag through a pile of cards rather than scroll past them → use Card Stack instead
- Steps need a pinned visual that changes beside the text → use Sticky Scroll instead
- There are many cards; beyond a handful the stack becomes a long, tiring scroll
Install
node scripts/add.mjs scroll-stack --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["scroll-stack"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, core/observe.js, components/scroll-stack/scroll-stack.js, components/scroll-stack/scroll-stack.css.
Usage
Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div id="mv-scroll-stack-demo" class="mv-scroll-area" data-orientation="vertical" tabindex="0" aria-label="Project process, scrollable">
<style>
#mv-scroll-stack-demo { width: min(100%, 44rem); height: 460px; margin-inline: auto; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg); }
#mv-scroll-stack-demo .intro { padding: 2.25rem 1.75rem 1.5rem; }
#mv-scroll-stack-demo .eyebrow { margin: 0; font: 500 .75rem/1 var(--mv-font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--mv-fg-muted); }
#mv-scroll-stack-demo h2 { margin: .6rem 0 .5rem; font: 650 1.75rem/1.1 var(--mv-font-display); letter-spacing: -.035em; text-wrap: balance; }
#mv-scroll-stack-demo .intro p { margin: 0; max-width: 30rem; color: var(--mv-fg-muted); font-size: .9rem; line-height: 1.55; }
#mv-scroll-stack-demo mv-scroll-stack { padding-inline: 1.5rem; --mv-scroll-stack-top: 1.25rem; --mv-scroll-stack-gap: 1.5rem; }
#mv-scroll-stack-demo .mv-scroll-stack-card { min-height: 15rem; display: grid; grid-template-rows: auto 1fr auto; gap: 1rem; padding: 1.75rem; }
#mv-scroll-stack-demo .top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; font: 500 .75rem/1 var(--mv-font-mono); color: var(--mv-fg-muted); }
#mv-scroll-stack-demo .step { display: inline-flex; align-items: center; gap: .55rem; color: var(--mv-fg); }
#mv-scroll-stack-demo .step::before { content: ""; width: .5rem; height: .5rem; border-radius: 50%; background: var(--mv-accent); }
#mv-scroll-stack-demo h3 { margin: 0 0 .5rem; font: 650 1.4rem/1.15 var(--mv-font-display); letter-spacing: -.025em; }
#mv-scroll-stack-demo .mv-scroll-stack-card p { margin: 0; max-width: 28rem; color: var(--mv-fg-muted); font-size: .9rem; line-height: 1.6; }
#mv-scroll-stack-demo ul { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 1rem 0 0; list-style: none; border-top: 1px dashed var(--mv-border); }
#mv-scroll-stack-demo li { padding: .25rem .6rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-full); font-size: .75rem; color: var(--mv-fg-muted); }
#mv-scroll-stack-demo .outro { padding: 0 1.75rem 3rem; color: var(--mv-fg-muted); font-size: .85rem; }
</style>
<div class="intro">
<p class="eyebrow">How we work</p>
<h2>Four steps, zero surprises</h2>
<p>Scroll the panel: each step slides over the previous one, which settles back into the deck.</p>
</div>
<mv-scroll-stack>
<article class="mv-scroll-stack-card" aria-labelledby="mv-ss-demo-1">
<div class="top"><span class="step">Step 01</span><span>Week 1</span></div>
<div>
<h3 id="mv-ss-demo-1">Discovery</h3>
<p>Stakeholder interviews, an audit of what already exists and a journey map. You leave with a ranked list of priorities.</p>
</div>
<ul aria-label="Deliverables"><li>Workshops</li><li>UX audit</li><li>Roadmap</li></ul>
</article>
<article class="mv-scroll-stack-card" aria-labelledby="mv-ss-demo-2">
<div class="top"><span class="step">Step 02</span><span>Weeks 2-4</span></div>
<div>
<h3 id="mv-ss-demo-2">Design</h3>
<p>Interactive mockups, a small design system and usability tests on a prototype before a single line of production code.</p>
</div>
<ul aria-label="Deliverables"><li>Prototype</li><li>Design system</li><li>User testing</li></ul>
</article>
<article class="mv-scroll-stack-card" aria-labelledby="mv-ss-demo-3">
<div class="top"><span class="step">Step 03</span><span>Weeks 5-10</span></div>
<div>
<h3 id="mv-ss-demo-3">Build</h3>
<p>Weekly releases to staging, code reviews on every change and accessibility checked continuously, not at the end.</p>
</div>
<ul aria-label="Deliverables"><li>Sprints</li><li>QA</li><li>WCAG 2.2 AA</li></ul>
</article>
<article class="mv-scroll-stack-card" aria-labelledby="mv-ss-demo-4">
<div class="top"><span class="step">Step 04</span><span>Week 11</span></div>
<div>
<h3 id="mv-ss-demo-4">Launch</h3>
<p>A guided go-live, analytics from day one and hands-on training so your team can take it from there.</p>
</div>
<ul aria-label="Deliverables"><li>Go-live</li><li>Analytics</li><li>Training</li></ul>
</article>
</mv-scroll-stack>
<p class="outro">The deck is complete and scrolls away with the rest of the page.</p>
</div>API
Attributes
| Name | Type | Description |
|---|---|---|
scroller | selector | window | Reference scroll container. Default: the nearest scroll container, or the page. Setting it uses the scroll-listener path. |
fallback | boolean | Forces the scroll-listener path instead of the scroll-driven animation (tests, older engines). |
Content structure
| Name | Description |
|---|---|
direct children | The cards, in stacking order. |
CSS classes
| Name | Description |
|---|---|
mv-scroll-stack-card | Default card look (optional): radius, raised surface, hairline border, shadow cast upward. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-scroll-stack-top | var(--mv-space-6) | Sticky position of the first card. |
--mv-scroll-stack-offset | 0.85rem | Vertical offset between two stacked cards. |
--mv-scroll-stack-scale | 0.05 | Scale reduction per covering card (floor 0.5). |
--mv-scroll-stack-dim | 0.1 | Dimming per covering card (capped at 0.45). |
--mv-scroll-stack-enter | 0.03 | Extra scale an incoming card settles from while it slides over the previous one (0 = none). |
--mv-scroll-stack-gap | var(--mv-space-8) | Space between cards before stacking. |
--mv-scroll-stack-tail | var(--mv-space-16) | Margin after the last card: how long the full stack stays visible. |
--mv-scroll-stack-i / -n | Card index and count, written by the JS (read-only). |
Accessibility
Reading and tab order unchanged; scrolling stays native (position: sticky, no scroll hijacking, no smooth-scroll library). Keyboard focus that lands in a covered card scrolls back just enough to show that card whole. Reduced motion: a plain list, nothing sticks, scales or dims. Forced colors: opaque outlined cards, no dimming.