Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

scroll-hero

English Β· TΓΌrkΓ§e

Scroll-driven canvas frame sequence β€” zero dependencies, drop-in for any site.

CI License: MIT Dependencies: 0

A self-contained package that turns a sequence of numbered WebP frames (extracted from a video) into a sticky, scroll-driven hero banner. Works on any page, at any insertion point, with no framework required.

πŸ‘¨β€πŸ’» Created by Remilya πŸš€ Need a premium website redesign or custom AI-agent integrations? Hire me.


Demo

Live: remilya.dev β€” the hero banner on the homepage is this engine running in production.

scroll-hero in action β€” scrolling down through the sticky banner and back

GIF is sped up and cut to 8 fps to keep it small. Full-quality recording: demo/scroll-hero-demo.mp4 (1 MB, 11 s, no audio).


60-second quickstart

# 1. Generate frames from a video (requires ffmpeg)
#    Windows:
powershell -File scripts/extract-frames.ps1 -InputVideo path/to/video.mp4 -OutputDir example/frames -DesktopWidth 1280 -DesktopHeight 720

#    macOS/Linux:
chmod +x scripts/extract-frames.sh && ./scripts/extract-frames.sh -i path/to/video.mp4 -o example/frames -w 1280 -h 720

# 2. Open the demo page (example/index.html reads ./frames)
open example/index.html

# 3. Optional: check the engine still behaves
node test/smoke.mjs

That's it. The engine auto-initialises any element with data-scroll-hero.

Sizes are a ceiling, not a target: don't ask for frames larger than your source β€” 1920Γ—1080 from a 720p clip is pure weight. Both scripts warn if you do.

No sample video or pre-rendered frames ship with the repo β€” point the extractor at your own clip and the demo page picks the frames up from example/frames.


How to make the video

Scroll position is mapped linearly onto the clip's timeline, so the source footage decides whether the effect feels premium or cheap.

What the engine wants

  • One continuous shot, no cuts, no scene changes.
  • 5–15 seconds. Longer clips mean more frames for the same scroll distance, not a longer animation.
  • A camera move that travels in a single direction β€” pull back, push in, slow orbit, crane down. A shot that reverses direction reads as a glitch once the user scrubs it.
  • 16:9, 1080p or better. The extractor can only downscale; it warns if you ask for more than the source has.
  • A strong opening frame: frame-001 doubles as the poster and is all that reduced-motion, Save-Data and no-JavaScript visitors ever see.

Generating one with AI (fastest route, no camera needed)

  • Google Flow / Veo β€” free tier, currently the easiest way to get a usable clip.
  • Seedance 2.5, Kling, Runway, Sora β€” any capable text-to-video or image-to-video model works.

Prompt tips: describe one continuous camera movement and say so explicitly ("a single continuous shot, no cuts, the camera slowly pulls back"). Feeding a reference still or a storyboard image of the move you want helps far more than adjectives. Expect to iterate β€” models like to sneak in cuts and to recreate reference images literally.

Or build it by hand β€” animate a still or a composition in After Effects / Premiere Pro, render a camera move out of Blender or Cinema 4D, or just shoot a real dolly or drone pass. Anything that exports an MP4 is fine.

Then extract the frames

./scripts/extract-frames.sh -i assets/hero-source.mp4 -o example/frames -n 48 -w 1280 -h 720

Set data-frame-count to the same number you passed to -n (default 48; 36–60 is the useful band β€” more frames buy smoothness and cost bytes). Use -s / -d to trim so the interesting part of the move spans the whole scroll. Dark, soft, motion-blurred footage compresses dramatically better than bright high-frequency detail; aim for ≀ ~1–2 MB of frames on mobile and ~3–5 MB on desktop.

Keep native scrolling in charge β€” the engine only reads scroll position and never hijacks it. Do not layer a scroll-jacking library on top.

Background reading: Steve Sewell's "Create a 3d scrolling animation with GSAP and Veo 3" on the Builder.io blog walks through the same frame-sequence technique with GSAP instead of this engine.


Folder layout

scroll-hero/
β”œβ”€ README.md                                    # This file
β”œβ”€ README.tr.md                                 # Turkish translation
β”œβ”€ HOW-TO-SCROLLING-VIDEO-HERO-BANNER.md        # Full guide
β”œβ”€ HOW-TO-SCROLLING-VIDEO-HERO-BANNER.tr.md     # Full guide (Turkish)
β”œβ”€ PROMPT.md                                    # One-send AI prompt
β”œβ”€ CHECKLIST.md                                 # Pre-ship QA
β”œβ”€ CHECKLIST.tr.md                              # Pre-ship QA (Turkish)
β”œβ”€ LICENSE                                      # MIT
β”œβ”€ package.json                                 # npm metadata / exports map
β”œβ”€ demo/                                        # Demo GIF + full-quality recording
β”œβ”€ src/
β”‚  β”œβ”€ scroll-hero.js                            # Standalone engine (classic script)
β”‚  β”œβ”€ scroll-hero.mjs                           # ESM entry for bundlers
β”‚  β”œβ”€ scroll-hero.css                           # Scoped styles
β”‚  └─ scroll-hero.html                          # Markup snippet
β”œβ”€ scripts/
β”‚  β”œβ”€ extract-frames.ps1                        # Windows frame extractor
β”‚  └─ extract-frames.sh                         # macOS/Linux frame extractor
β”œβ”€ test/
β”‚  └─ smoke.mjs                                 # Zero-dependency engine test
β”œβ”€ .github/workflows/ci.yml                     # Runs the smoke test on push/PR
└─ example/
   β”œβ”€ index.html                                # Working demo
   └─ frames/                                   # Drop generated frames here

Installation

Plain HTML

<!-- 1. Link the CSS -->
<link rel="stylesheet" href="path/to/scroll-hero.css" />

<!-- 2. Add the banner markup -->
<div class="scroll-hero" data-scroll-hero data-frame-count="48" data-frame-dir="frames">
  <div class="scroll-hero__sticky">
    <canvas class="scroll-hero__canvas" aria-hidden="true"></canvas>
    <picture class="scroll-hero__fallback">
      <img src="frames/frame-001.webp" alt="" loading="eager" />
    </picture>
    <div class="scroll-hero__veil" aria-hidden="true"></div>
    <div class="scroll-hero__content">
      <h1>Headline</h1>
      <p>Subtitle</p>
    </div>
  </div>
</div>

<!-- 3. Include the script -->
<script src="path/to/scroll-hero.js"></script>

npm / ESM / bundler

npm i scroll-hero

(Not on npm yet β€” until it is published, use the CDN snippet below or copy src/ into your project. Everything else in this section already works from a local path.)

import ScrollHero from 'scroll-hero';   // resolves to src/scroll-hero.mjs
import 'scroll-hero/css';
ScrollHero.initAll();

src/scroll-hero.js is a classic script with no export, so importing that file directly gives you undefined in a real ESM setup β€” use scroll-hero.mjs, which the package's import condition already points at. There is no window on the server, so the default export is undefined during SSR; guard your effect or import lazily.

CDN (no build step)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Remilya/scroll-hero@v1.0.0/src/scroll-hero.css" />
<script src="https://cdn.jsdelivr.net/gh/Remilya/scroll-hero@v1.0.0/src/scroll-hero.js"></script>

Pin a tag as above rather than @main, so a later commit cannot change what your page loads.

React / Vue / Svelte / any SPA

Turn the auto-init off and own the lifecycle, or the DOMContentLoaded init and the MutationObserver will fight your framework:

window.ScrollHeroNoAutoInit = true;
window.ScrollHeroNoObserve = true;

// in the component
const instance = ScrollHero.create(ref.current); // null on the static path
return () => instance?.destroy();

Full guidance β€” asset paths, static exports, Strict Mode, SSR β€” in the Framework Integration section of the HOW-TO.

CMS / No-build (paste snippet)

See the full markup in src/scroll-hero.html β€” copy the div block, customise the data-* attributes, link the CSS and JS, done.


Configuration

All via data-* attributes on the .scroll-hero element. The canonical reference β€” every attribute, the engine's own attributes, and the JavaScript API β€” is the Data-Attribute Reference in the HOW-TO. The most-used ones:

Attribute Default Description
data-frame-count (required) Total frames in sequence
data-frame-dir frames Base frames directory (use an absolute path if the site has a base path)
data-frame-dir-desktop / -mobile (=base) Per-breakpoint overrides
data-breakpoint 720 Viewport width breakpoint (px)
data-initial-batch 6 Frames to preload on init
data-scroll-length 300 Sticky height in vh
data-overlay-fade 20 % scroll over which content fades
data-version "" Cache-busting query param

Behaviour

  • Sticky + scroll progress: the banner occupies Nvh of scroll space; a sticky inner container (100svh) stays fixed while the user scrolls through it. Progress is -top / (bannerHeight - stickyHeight) β€” 0 at rest, 1 when the sticky container releases β€” so it behaves the same at the top of the page or mid-page.
  • 1-based frames: progress 0 draws frame-001, progress 1 draws frame-<count>, matching what the extractors write.
  • Progressive loading: the first data-initial-batch frames load immediately, the rest follow in a background queue. Renders the nearest already-loaded frame so scrubbing never blocks.
  • Layering: the poster sits below the canvas and stays there; the canvas is revealed (data-scroll-hero-ready) only once a frame has been drawn, so there is no blank flash and nothing ever covers the animation or the content.
  • Responsive: separate desktop/mobile frame directories with a configurable breakpoint; switching breakpoints reloads the sequence.
  • Fallbacks: no canvas, no 2d context, too few frames β†’ static poster; prefers-reduced-motion or saveData β†’ poster only, zero frame downloads. Individual frame failures are skipped silently.
  • Multiple instances: every [data-scroll-hero] element initialises independently, with its own listeners and throttles.
  • Off-screen pause: an IntersectionObserver stops all measuring and drawing while the banner is out of view β€” no getBoundingClientRect() per scroll event on a long page β€” and re-syncs the moment it comes back.
  • One poster download: if the <img> in your markup already points at the poster file, the engine leaves its src alone instead of rewriting it with ?v=, which would fetch the same image twice. Put the version query in the markup too when you bump data-version.
  • Clean teardown: destroy() removes every listener, timer and observer, aborts in-flight image loads and clears the canvas.
  • Custom event: scrollhero:progress fires on the element with { progress: 0-1, frameIndex } whenever the frame index changes.

Recipes

The engine writes --scroll-hero-progress (0–1) onto the banner element and fires scrollhero:progress, so most "extra" effects need no engine changes and no new attributes.

Parallax layers β€” any element inside the banner can ride the same progress value:

.my-layer {
  transform: translateY(calc(var(--scroll-hero-progress) * -60px));
}

Come into focus / colour as the user scrolls:

.scroll-hero__canvas {
  filter: grayscale(calc((1 - var(--scroll-hero-progress)) * 100%));
}

Analytics milestones (GA4 / GTM) from the existing event:

const sent = new Set();
document.addEventListener('scrollhero:progress', (event) => {
  const percent = Math.floor(event.detail.progress * 4) * 25; // 0 / 25 / 50 / 75 / 100
  if (percent && !sent.has(percent)) {
    sent.add(percent);
    window.dataLayer?.push({ event: 'scroll_hero_progress', percent });
  }
});

Accessibility

  • Canvas is aria-hidden="true".
  • Veil is aria-hidden="true".
  • Fallback <picture> uses alt="" (decorative).
  • prefers-reduced-motion: reduce disables the entire canvas engine β€” static poster only.
  • navigator.connection.saveData also disables frame downloads.
  • @media (scripting: none) gives the same static hero when JavaScript is unavailable.
  • Content heading should be a meaningful <h1>–<h3> in document order.

Requirements

  • Any modern browser (Chrome, Firefox, Safari, Edge) β€” IE11 is not supported.
  • ffmpeg + ffprobe in PATH (for frame extraction via the scripts).
  • bash, bc and awk for extract-frames.sh; PowerShell 5+ for extract-frames.ps1.
  • Node 18+ to run test/smoke.mjs (no packages required).

License

MIT β€” see LICENSE. Free to use, modify, and distribute; keep the copyright notice. Copyright (c) 2026 Remilya.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages