🎬 CSS scroll-driven animations
Scroll Cinema
The scrollbar becomes the animation timeline. Progress bars, parallax, staged reveals — all running off-main-thread, no IntersectionObserver, no scroll listeners, no jank.
Use it for: Storytelling longform · product pages · annual reports
Resistance
Every mind has inertia. The first force is naming the thing that holds you still.
Momentum
Mass times velocity. Small daily pushes compound into unstoppable motion.
Flow
Equal and opposite — the world pushes back exactly as hard as you push it.
Every animation you just saw was pure CSS.
No JavaScript ran. The scrollbar was the timeline.
The complete source
This is the exact file rendering the demo above — a single self-contained React client component with its styles inline. Copy or download it, drop it into any React / Next.js project, and it runs. No extra dependencies.
1"use client";23/* ============================================================4SCROLL CINEMA — 100% CSS scroll-driven animations.5Zero JavaScript drives any motion here: every effect binds to6`animation-timeline: view()` / `scroll()` (Baseline-ish 2026:7Chrome, Edge, Safari 26; Firefox behind a flag — wrapped in8@supports so it degrades to a static page gracefully).9============================================================ */1011const SCENES = [12{13title: "Resistance",14text: "Every mind has inertia. The first force is naming the thing that holds you still.",15hue: "#7c3aed",16},17{18title: "Momentum",19text: "Mass times velocity. Small daily pushes compound into unstoppable motion.",20hue: "#06b6d4",21},22{23title: "Flow",24text: "Equal and opposite — the world pushes back exactly as hard as you push it.",25hue: "#ffa500",26},27];2829export default function ScrollStory() {30return (31<div className="story-wrap">32<style>{`33.story-wrap { position: relative; }3435/* progress bar bound to THIS scroller, not the page */36.story-scroller {37height: 70svh; overflow-y: auto; border-radius: 24px;38border: 1px solid rgba(255,255,255,.15);39background: #0a0a12;40scroll-timeline: --story block; /* named scroll timeline */41}42.story-bar {43position: sticky; top: 0; z-index: 5; height: 4px; width: 100%;44transform-origin: 0 50%; transform: scaleX(0);45background: linear-gradient(90deg,#7c3aed,#06b6d4,#ffa500);46}47@supports (animation-timeline: scroll()) {48.story-bar { animation: story-grow linear both; animation-timeline: --story; }49}50@keyframes story-grow { to { transform: scaleX(1); } }5152.story-scene {53min-height: 95svh; display: grid; place-items: center;54padding: 4rem 2rem; position: relative; overflow: clip;55}56.story-card { max-width: 56ch; text-align: center; position: relative; z-index: 2; }57.story-title { font-size: 4.8rem; font-weight: 900; letter-spacing: -.02em; }58.story-text { font-size: 1.8rem; line-height: 1.7; color: rgba(255,255,255,.78); margin-top: 1.6rem; }5960/* each scene's halo scales/rotates as it crosses the viewport */61.story-halo {62position: absolute; inset: 10%; border-radius: 50%;63filter: blur(60px); opacity: .5; z-index: 1;64}65@supports (animation-timeline: view()) {66.story-card {67animation: scene-in linear both;68animation-timeline: view(block);69animation-range: entry 10% entry 60%;70}71.story-halo {72animation: halo-spin linear both;73animation-timeline: view(block);74animation-range: cover 0% cover 100%;75}76.story-num {77animation: num-slide linear both;78animation-timeline: view(block);79animation-range: cover 20% cover 80%;80}81}82@keyframes scene-in {83from { opacity: 0; transform: translateY(80px) scale(.92); }84to { opacity: 1; transform: none; }85}86@keyframes halo-spin {87from { transform: rotate(0deg) scale(.6); }88to { transform: rotate(180deg) scale(1.15); }89}90@keyframes num-slide {91from { transform: translateX(-30%); opacity: .04; }92to { transform: translateX(10%); opacity: .12; }93}94.story-num {95position: absolute; left: 0; top: 50%; translate: 0 -50%;96font-size: 28rem; font-weight: 900; opacity: .08; z-index: 0;97pointer-events: none; line-height: 1;98}99.story-end {100min-height: 50svh; display: grid; place-items: center;101font-size: 2rem; color: rgba(255,255,255,.6); text-align: center;102}103`}</style>104105<div className="story-scroller" tabIndex={0} aria-label="Scroll story">106<div className="story-bar" aria-hidden="true" />107{SCENES.map((s, i) => (108<section className="story-scene" key={s.title}>109<div110className="story-halo"111style={{ background: `radial-gradient(circle, ${s.hue}, transparent 70%)` }}112aria-hidden="true"113/>114<div className="story-num" aria-hidden="true">115{i + 1}116</div>117<div className="story-card">118<h3 className="story-title" style={{ color: s.hue }}>119{s.title}120</h3>121<p className="story-text">{s.text}</p>122</div>123</section>124))}125<div className="story-end">126<p>127Every animation you just saw was pure CSS.128<br />129No JavaScript ran. The scrollbar was the timeline.130</p>131</div>132</div>133</div>134);135}136