| name | scroll-sequence |
| description | Turn a video file into a premium scroll-sequence website — the Apple-style experience where scrolling scrubs through video frames with choreographed text animations. Use this skill whenever the user provides a video file (MP4, MOV, WebM, etc.) and wants it turned into a scroll-sequence site, scroll-driven landing page, or scrollytelling experience. Also triggers when the user mentions "scroll sequence," "scrollytelling," "scroll-driven animation," "video frames on scroll," "Apple-style scroll animation," "frame-by-frame scroll," or wants a product reveal site built from video footage. If the user has a product video, demo reel, or any footage they want presented as a polished scroll-sequence web experience rather than just an embedded video player, this is the skill to use. |
Video to Premium Scroll-Driven Website
Turn a video file into a scroll-driven animated website with animation variety and choreography — multiple animation types working together, not one repeated effect.
The result is a website where scrolling controls video playback frame-by-frame on a canvas, with text sections animating in from different directions as the user scrolls. Think Apple product pages, but generated from any video — product demos, brand films, nature footage, music videos, or anything visual.
Scope
This skill does:
- Extract frames from a video and build a scroll-driven single-page site
- Generate HTML, CSS, and JS with no build tools (vanilla + CDN)
- Handle product/brand sites, editorial pages, artistic showcases
This skill does not:
- Edit or trim the source video
- Add audio/sound sync
- Build multi-page sites, dashboards, or e-commerce stores
Input
The user provides a video file path (MP4, MOV, WebM, etc.) and optionally:
- A theme/brand name
- Desired text sections and where they appear
- Color scheme preferences
- Any specific design direction (commercial product page, editorial, artistic)
If the user doesn't specify these, ask briefly or use sensible creative defaults.
Premium Defaults
These are strong defaults backed by visual reasoning. Each solves a specific problem. If the user explicitly requests something different, follow their preference — they know their context better.
- Lenis smooth scroll — native browser scroll feels jerky during frame playback; Lenis provides the buttery interpolation that makes canvas animation feel intentional
- 4+ animation types — repeating the same entrance animation makes sections feel templated; variety creates the impression each section was hand-crafted
- Staggered reveals — label then heading then body then CTA; simultaneous entrance looks like a flash, staggering guides the eye through a reading hierarchy
- No glassmorphism cards — frosted-glass effects over video frames create visual noise; clean backgrounds with typographic hierarchy let the video breathe
- Direction variety — sections entering from different directions (left, right, up, scale, clip) create spatial depth; same-direction entrances feel flat
- Dark overlay for stats — statistics need high contrast to read over moving frames; 0.88-0.92 opacity overlay with animated counters. This is the one context where centered text works
- Horizontal text marquee — at least one oversized text element (12vw+) sliding on scroll; this breaks the vertical rhythm and creates visual surprise
- Counter animations — numbers count up from 0 on scroll; static numbers feel dead next to animated video frames
- Massive typography — hero 12rem+, section headings 4rem+, marquee 10vw+; at these sizes type becomes a design element, not just text
- CTA persists —
data-persist="true" keeps the final section visible after it animates in
- Hero prominence + generous scroll — hero gets 20%+ scroll range, 800vh+ total for 6 sections; cramped scroll ranges make animations feel rushed
- Side-aligned text — text in outer 40% zones (
align-left/align-right) so the video occupies the viewport center without competition. Exception: stats with full dark overlay
- Circle-wipe hero reveal — hero is a standalone 100vh section; the canvas reveals via expanding
clip-path: circle() as the hero scrolls away
- Video completion point — by default the video plays across the entire scroll (
FRAME_SPEED: 1.0). If the user wants the video to finish earlier (e.g., "video should finish at 80% scroll"), calculate FRAME_SPEED = 100 / desired_percent:
- 100% →
FRAME_SPEED: 1.0 (default, full sync)
- 80% →
FRAME_SPEED: 1.25
- 60% →
FRAME_SPEED: 1.67
- 50% →
FRAME_SPEED: 2.0
After the video finishes, the last frame stays frozen for the remaining scroll. The dark scrim ensures text stays readable over both live and frozen frames
Workflow
Step 1: Verify Dependencies
which ffmpeg && which ffprobe
If not found, tell the user to install ffmpeg before proceeding.
Step 2: Analyze the Video
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration,r_frame_rate,nb_frames -of csv=p=0 "<VIDEO_PATH>"
Determine resolution, duration, frame rate, total frames. Decide:
- Target frame count: 150-300 frames for good scroll experience
- Short video (<10s): extract at original fps, cap at ~300
- Medium (10-30s): extract at 10-15fps
- Long (30s+): extract at 5-10fps
- Output resolution: Match aspect ratio, cap width at 1920px
- File size check: At 80% webp quality, budget ~30-80KB per frame. 300 frames at 1920px wide runs ~15-25MB total. If the video would produce 40MB+, reduce resolution to 1280px or cut frame count to stay under that.
Step 3: Extract Frames
mkdir -p frames
ffmpeg -i "<VIDEO_PATH>" -vf "fps=<CALCULATED_FPS>,scale=<WIDTH>:-1" -c:v libwebp -quality 80 "frames/frame_%04d.webp"
After extraction, verify the count: ls frames/ | wc -l
Step 4: Scaffold
Create an output directory named after the project (e.g., the brand name, or video-site as fallback):
<project-name>/
index.html
css/style.css
js/app.js
frames/frame_0001.webp ...
No bundler. Vanilla HTML/CSS/JS + CDN libraries. This keeps it portable and zero-config.
Step 5: Build index.html
Required <head> content:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="[PROJECT_DESCRIPTION]">
<meta property="og:title" content="[PROJECT_NAME]">
<meta property="og:description" content="[PROJECT_DESCRIPTION]">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>EMOJI</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
Required <body> structure (in this order):
1. Loader — #loader > .loader-brand, #loader-bar-track > #loader-bar, #loader-percent
2. Fixed header — .site-header > nav with logo + links
3. Hero — .hero-standalone (100vh, solid bg, word-split heading)
4. Canvas — .canvas-wrap > canvas#canvas (fixed, full viewport)
5. Dark overlay — #dark-overlay (fixed, full viewport, pointer-events:none)
6. Marquee(s) — .marquee-wrap > .marquee-text (fixed, 12vw font)
7. Scroll container — #scroll-container (800vh+)
Content sections with data-enter, data-leave, data-animation
Stats section with .stat-number[data-value][data-decimals]
CTA section with data-persist="true"
Read references/implementation.md for HTML templates (content sections, stats sections, CDN script tags).
Step 6: Build css/style.css
Key concerns:
- Side-aligned text zones:
.align-left and .align-right keep text in the outer 40%, leaving center for video
- Scroll sections:
position: absolute within scroll container, positioned at midpoint of enter/leave range
- Text contrast over video: Text floats over canvas frames that can be any brightness. Two rules:
- Nearly-opaque panel: Always add
background: rgba(0,0,0,0.92) !important on .section-inner. The !important is needed because GSAP's inline style cascade can interfere. This reads as a solid dark card with just a hint of the video at the edges. Subtle scrims (0.5-0.8) are NOT enough — video frames are busy and bright.
- No opacity reduction on text: Do NOT set
opacity below 0.85 on .section-label, .section-body, or .section-note. These elements sit over video — reducing opacity makes them gray and invisible on bright frames. Use color values for hierarchy instead (e.g. #fff for headings, #ddd for body, #aaa for labels).
- Font and color choices: Be creative and distinctive, complement the video content
Read references/implementation.md (CSS Patterns section) for the full CSS including mobile breakpoints.
Step 7: Build js/app.js
The JavaScript has 9 components. Read references/implementation.md for the complete code. Here's what each does and why:
| Component | Purpose |
|---|
| Lenis setup | Smooth scroll interpolation — mandatory for frame playback to feel good. Must include smoothTouch: true for iOS Safari |
| Frame preloader | Two-phase: 10 frames fast, then the rest. Shows progress bar. |
| Canvas renderer | Padded cover mode (IMAGE_SCALE 0.82-0.90) with background color sampled from frame edges |
| Frame-to-scroll binding | Maps scroll progress to frame index with FRAME_SPEED acceleration |
| Section animations | Reads data-animation attribute, plays entrance on enter, reverses on leave (unless persist) |
| Counter animations | Numbers count up from 0 using GSAP snap |
| Marquee | Horizontal sliding text with scroll-linked fade in/out |
| Dark overlay | Fades to 0.9 opacity over the stats section range |
| Circle-wipe | Expands clip-path circle to reveal canvas as hero scrolls away |
Step 8: Test
- Serve locally:
npx serve . or python3 -m http.server 8000
- Scroll through fully — verify each section has a DIFFERENT animation type
- Confirm: smooth scroll, frame playback, staggered reveals, marquee slides, counters count up, dark overlay fades, CTA persists at end
Step 9: Deploy (optional)
If the user wants the site live, choose a deployment target:
Option A — Vercel (default, no auth required):
bash skills/vercel-deploy/scripts/deploy.sh <project-directory>
Vercel's deploy endpoint has a ~4.5MB compressed payload limit. Full-resolution scroll-sequence sites (1920px, 200 frames) typically exceed this. Before deploying to Vercel, re-extract frames at deploy-friendly resolution:
ffmpeg -y -i video.mp4 -vf "fps=12,scale=960:-1" -c:v libwebp -quality 65 frames/frame_%04d.webp
Then update FRAME_COUNT in js/app.js to match the new count. A typical 8s video at 960px/12fps = ~96 frames, ~2MB compressed. No need to change FRAME_SPEED — it stays at 1.0 regardless of frame count.
Option B — AWS S3 + CloudFront (no size limit):
bash skills/aws-deploy/scripts/deploy.sh <project-directory> [--region us-east-1]
AWS has no payload size limit — deploy full-resolution 1920px frames without downsampling. Use this when the user requests AWS hosting, or when asset size exceeds Vercel's 4.5MB limit and the user doesn't want to reduce quality. Requires AWS CLI and credentials. First deploy takes 3-5 minutes; subsequent deploys reuse the same stack. If the aws-deploy skill is installed at a different path, look for it at ../aws-deploy/scripts/deploy.sh relative to this skill, or in .claude/skills/aws-deploy/scripts/.
Option C — Google Cloud / Firebase (no size limit):
bash skills/gcp-deploy/scripts/deploy.sh <project-directory> [project-id]
GCP/Firebase has no strict payload size limit for Hosting — deploy full-resolution frames without downsampling. Use this when the user requests GCP or Firebase hosting, or when asset size exceeds Vercel's 4.5MB limit. Requires Firebase CLI and local authentication. If the gcp-deploy skill is installed at a different path, look for it at ../gcp-deploy/scripts/deploy.sh relative to this skill, or in .claude/skills/gcp-deploy/scripts/.
Mobile Considerations
Mobile is where scroll-driven video sites break hardest. These are requirements, not suggestions:
- Memory: Mobile browsers aggressively kill tabs using too much memory. Use
matchMedia("(max-width: 767px)") to load a reduced frame set on mobile — cap at 150 frames and 1280px resolution. The implementation reference includes a getMobileFrameCount() helper for this.
- Touch scrolling: Lenis must include
smoothTouch: true — without it, iOS Safari's momentum scrolling fights with Lenis and causes choppy frame playback.
- Canvas scaling: On tall mobile viewports (9:19.5 aspect), the desktop
IMAGE_SCALE: 0.85 causes excessive letterboxing — the video appears small with large background bars. Increase to IMAGE_SCALE: 0.95 on mobile so the video fills the screen. The implementation reference includes a responsive scale calculation.
- Layout: Collapse side-aligned text to full-width centered with a dark backdrop behind it (so text reads over the video). See the mobile CSS in
references/implementation.md.
- Scroll height: The mobile CSS reduces scroll height from 800vh+ to 550vh. Animations pass faster at 550vh — each 14% section range covers ~77vh instead of ~112vh. This is intentional; mobile users scroll faster and have less patience for long pages.
- Typography: Scale down massively — hero to ~3rem, headings to ~2rem. The desktop sizes will overflow on small screens.
- Viewport: Use
viewport-fit=cover in the meta tag so the canvas extends into the safe area on notched phones.
Animation Types Quick Reference
| Type | Initial State | Animate To | Duration |
|---|
fade-up | y:50, opacity:0 | y:0, opacity:1 | 0.9s |
slide-left | x:-80, opacity:0 | x:0, opacity:1 | 0.9s |
slide-right | x:80, opacity:0 | x:0, opacity:1 | 0.9s |
scale-up | scale:0.85, opacity:0 | scale:1, opacity:1 | 1.0s |
rotate-in | y:40, rot:3, opacity:0 | y:0, rot:0, opacity:1 | 0.9s |
stagger-up | y:60, opacity:0 | y:0, opacity:1 | 0.8s |
clip-reveal | clipPath:inset(100% 0 0 0) | clipPath:inset(0%) | 1.2s |
All use stagger (0.1-0.15s). Easing: power3.out (scale-up: power2.out, clip-reveal: power4.inOut).
Scroll Range Planning
With the default FRAME_SPEED: 1.0, the video plays across the entire scroll — every section has live video behind it. If the user requests an earlier completion point, sections after that point animate over the frozen last frame. Either way, the dark scrim on .section-inner ensures readability.
Formula for N content sections (excludes the hero, which is a standalone element):
| Zone | Scroll range | Purpose |
|---|
| Hero reveal | 0-7% | Hero fades out, circle-wipe reveals canvas |
| Content sections | 8-100% | Sections animate over video (live or frozen) |
Within the content zone, divide evenly with ~2% gaps between sections. Each section needs at least 8% range for animations to breathe.
Example for 6 sections:
| Section | enter | leave | animation | alignment |
|---|
| 1 (intro) | 10 | 24 | slide-left | align-left |
| 2 (feature) | 26 | 40 | slide-right | align-right |
| 3 (feature) | 42 | 54 | fade-up | align-left |
| 4 (stats) | 56 | 72 | stagger-up | (centered, dark overlay) |
| 5 (detail) | 74 | 86 | scale-up | align-right |
| 6 (CTA) | 88 | 100 | clip-reveal | align-left, persist |
Dark overlay enter/leave should match the stats section range (here: 0.56 to 0.72).
Quick rules:
- Hero gets scroll range via the circle-wipe (0-7%), not
data-enter/data-leave
- Never less than 8% range per section
- Never repeat the same
data-animation on consecutive sections
- Alternate
align-left / align-right (except stats, which are centered)
- Last section:
data-persist="true"
Anti-Patterns
- Cycling feature cards in a pinned section — each card gets too little scroll time. Give each feature its own section (8-10% scroll range) with its own animation type
- Pure cover mode (scale at 1.0) — clips into header. Use IMAGE_SCALE 0.82-0.90
- Pure contain mode — leaves visible border. Padded cover + bg sampling solves this
- FRAME_SPEED without user intent — default to 1.0 (full sync). Only increase if the user explicitly asks for the video to finish early (e.g., "video ends at 80%"). Calculate with
100 / desired_percent
- Hero < 20% scroll range — first impression needs breathing room
- Same animation on consecutive sections — never repeat the same entrance type back-to-back
- Text without a nearly-opaque backdrop over video — video frames are busy and bright; even
rgba(0,0,0,0.78) is too transparent. Use rgba(0,0,0,0.92) on .section-inner so it reads as a solid dark card. Never use gradients that fade below 0.9. Never reduce text opacity below 0.85 — use color values for hierarchy instead
- Wide centered grids over canvas — redesign as vertical lists in the 40% side zone
- Scroll height < 800vh for 6 sections — everything feels rushed
Clip-Path Variations
Beyond the default circle-wipe, other reveal options:
- Wipe from left:
inset(0 100% 0 0) -> inset(0 0% 0 0)
- Wipe from bottom:
inset(100% 0 0 0) -> inset(0% 0 0 0)
- Diamond:
polygon(50% 0%, 50% 0%, 50% 100%, 50% 100%) -> polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)
Accessibility
Troubleshooting
- Text scrim not visible / text unreadable over video: The
#scroll-container needs z-index: 8 (or higher than .canvas-wrap at z-index 5). Without it, the fixed canvas covers the scroll sections and the scrim background is invisible even though it's in the CSS.
- Frames not loading: Must serve via HTTP, not
file://
- Choppy scrolling: Increase
scrub value, reduce frame count
- White flashes: Ensure all frames loaded before hiding loader
- Blurry canvas: Apply
devicePixelRatio scaling to canvas dimensions
- Lenis conflicts: Ensure
lenis.on("scroll", ScrollTrigger.update) is connected
- Counters not animating: Verify
data-value attribute exists and snap matches decimal places
- Memory issues on mobile: Reduce frames to <150, resize to 1280px wide