Sticky Product Tour
Classic scrollytelling — a sticky device on the left swaps its screen to match each step as the copy scrolls past on the right.
Sticky Product Tour the 12th of 12 designs in the 12 CSS Scroll Animations collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="lead">
<span class="badge">Product Tour</span>
<h1>Watch the interface change as you read.</h1>
<p>A sticky device on the left holds its place while the steps on the right scroll past — each step swaps the screen to match.</p>
</div>
<section class="tour">
<div class="stage">
<div class="sticky">
<div class="device">
<div class="screen">
<div class="panel pA on" data-i="0">
<div class="glyph">🔍</div>
<div class="pt">Capture</div>
<div class="pd">Drop in any source — the workspace ingests it instantly.</div>
</div>
<div class="panel pB" data-i="1">
<div class="glyph">🧩</div>
<div class="pt">Organise</div>
<div class="pd">Auto-grouped into boards you can rearrange by hand.</div>
</div>
<div class="panel pC" data-i="2">
<div class="glyph">⚡</div>
<div class="pt">Automate</div>
<div class="pd">Rules fire on change — no manual upkeep.</div>
</div>
<div class="panel pD" data-i="3">
<div class="glyph">📤</div>
<div class="pt">Ship</div>
<div class="pd">Publish anywhere with one reviewed export.</div>
</div>
<div class="pips">
<span class="pip on"></span><span class="pip"></span>
<span class="pip"></span><span class="pip"></span>
</div>
</div>
</div>
</div>
</div>
<div class="steps">
<div class="step" data-step="0">
<div class="n">Step 01</div>
<h2>Capture everything in one inbox</h2>
<p>Links, files, notes, screenshots — anything you feed the workspace lands in a single capture stream, parsed and tagged the moment it arrives.</p>
</div>
<div class="step" data-step="1">
<div class="n">Step 02</div>
<h2>Organise without the busywork</h2>
<p>Captured items group themselves into boards. You stay in control — drag, merge, or split — but you never start from an empty canvas.</p>
</div>
<div class="step" data-step="2">
<div class="n">Step 03</div>
<h2>Automate the parts you repeat</h2>
<p>Set a rule once and it runs forever. When something changes upstream, the workspace reacts before you'd think to.</p>
</div>
<div class="step" data-step="3">
<div class="n">Step 04</div>
<h2>Ship to anywhere, reviewed</h2>
<p>A single export pipeline pushes to every destination — with a review gate so nothing leaves before you say so.</p>
</div>
</div>
</section> *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{background:#0A0A0F;font-family:'Sora',sans-serif;color:#E8E8EC}
.lead{
padding:8rem 6vw 5rem;max-width:760px;
}
.lead .badge{
display:inline-block;font-family:'IBM Plex Mono',monospace;
font-size:11px;letter-spacing:0.14em;text-transform:uppercase;
color:#34D399;border:1px solid rgba(52,211,153,0.3);
padding:5px 12px;border-radius:20px;margin-bottom:1.5rem;
}
.lead h1{
font-size:clamp(34px,5.5vw,62px);font-weight:700;
line-height:1.05;letter-spacing:-0.025em;
}
.lead p{margin-top:1.25rem;font-size:17px;color:#9A9AA5;line-height:1.7;max-width:54ch}
/* scrollytelling section: sticky visual + scrolling steps */
.tour{
display:grid;grid-template-columns:1fr 1fr;gap:4vw;
padding:0 6vw 8rem;
}
/* left: sticky device */
.stage{position:relative}
.sticky{
position:sticky;top:14vh;height:72vh;
display:flex;align-items:center;justify-content:center;
}
.device{
width:300px;height:430px;border-radius:34px;
background:#16161E;border:1px solid rgba(255,255,255,0.08);
padding:14px;position:relative;overflow:hidden;
box-shadow:0 40px 80px -30px rgba(0,0,0,0.8);
}
.screen{
width:100%;height:100%;border-radius:22px;overflow:hidden;
position:relative;background:#0A0A0F;
}
/* each panel stacked, cross-faded */
.panel{
position:absolute;inset:0;
display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:1rem;
opacity:0;transform:scale(1.05);
transition:opacity 0.6s ease,transform 0.6s ease;
padding:2rem;text-align:center;
}
.panel.on{opacity:1;transform:scale(1)}
.panel .glyph{font-size:54px}
.panel .pt{font-weight:600;font-size:18px}
.panel .pd{font-size:12px;color:#9A9AA5;line-height:1.6}
.pA{background:linear-gradient(160deg,#1E1B4B,#0A0A0F)}
.pB{background:linear-gradient(160deg,#064E3B,#0A0A0F)}
.pC{background:linear-gradient(160deg,#7C2D12,#0A0A0F)}
.pD{background:linear-gradient(160deg,#4C1D95,#0A0A0F)}
/* step progress pips on device frame */
.pips{
position:absolute;bottom:26px;left:50%;transform:translateX(-50%);
display:flex;gap:7px;
}
.pip{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,0.18);
transition:background 0.4s,width 0.4s}
.pip.on{background:#34D399;width:18px;border-radius:4px}
/* right: scrolling steps */
.steps{display:flex;flex-direction:column}
.step{
min-height:78vh;display:flex;flex-direction:column;justify-content:center;
}
.step .n{
font-family:'IBM Plex Mono',monospace;font-size:13px;
color:#34D399;margin-bottom:0.9rem;
}
.step h2{
font-size:clamp(24px,3vw,38px);font-weight:600;
letter-spacing:-0.015em;line-height:1.15;margin-bottom:1rem;
}
.step p{font-size:15px;color:#9A9AA5;line-height:1.75;max-width:42ch}
@media(max-width:760px){
.tour{grid-template-columns:1fr}
.sticky{height:auto;position:relative;top:0;margin-bottom:2rem}
} const panels=[...document.querySelectorAll('.panel')];
const pips=[...document.querySelectorAll('.pip')];
const steps=[...document.querySelectorAll('.step')];
function setActive(i){
panels.forEach(p=>p.classList.toggle('on',+p.dataset.i===i));
pips.forEach((p,j)=>p.classList.toggle('on',j===i));
}
const io=new IntersectionObserver((entries)=>{
entries.forEach(e=>{
if(e.isIntersecting) setActive(+e.target.dataset.step);
});
},{rootMargin:'-45% 0px -45% 0px'});
steps.forEach(s=>io.observe(s));