Film Strip Hero
A hero with a sprocketed film strip looping endlessly beneath the headline; the faster the page scrolls, the faster the film runs, and hovered frames enlarge.
A hero section for photography, film, event and portfolio home pages: a left-aligned title, copy and CTAs on top, and below them a dark film strip pinned to the bottom of the section that bleeds to both edges — two rows of sprocket holes with 3:2 frames in between, each stamped with an edge code like 01A or 02A in the bottom-left corner. The film loops sideways at a steady speed, runs faster the faster the page scrolls, and reverses when you scroll up; rest the pointer on a frame and it enlarges while the rest dim, and the film springs down to 20% speed. Without images, every frame draws a warm gradient and a geometric scene placeholder.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add film-strip-hero.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<FilmStripHero/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/film-strip-hero.en.jsonOr, once registries are configured in components.json, install it as @webberui/film-strip-hero.
Usage
import { FilmStripHero } from "@/components/ui/film-strip-hero";
<FilmStripHero
eyebrow="Sandbar Pictures · Spring Screening 2026"
title="One roll of film, twenty-four frames of everyday life"
subtitle="A year of work from eight street photographers, screened back to back."
primaryAction={{ label: "Book a visit", href: "/tickets" }}
secondaryAction={{ label: "See the lineup", href: "/lineup" }}
frames={[
{ src: "/photos/harbor.jpg", alt: "A fishing harbour at dawn" },
{ src: "/photos/market.jpg", alt: "Stalls at an evening market" },
{ src: "/photos/alley.jpg", alt: "An old alley after the rain" },
]}
speed={48}
/>Inside a nested scroll container
By default the component tracks window scroll. When it sits inside a fixed-height overflow-y-auto container, pass that container's ref to container. The velocityBoost speed-up is driven by the container's scroll velocity instead of the window's.
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[400px] overflow-y-auto">
<FilmStripHero container={scrollerRef} />
</div>When the window itself is the scroll container, container can be omitted and behavior is unchanged.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Main title (required) |
eyebrow | ReactNode | — | Small label above the title, in the accent colour |
subtitle | ReactNode | — | Supporting copy below the title |
primaryAction | FilmStripHeroAction | — | Primary action with an accent-coloured background |
secondaryAction | FilmStripHeroAction | — | Outlined secondary action |
as | "h1" | "h2" | "h1" | Heading level |
minHeight | number | 560 | Minimum section height (px); the film strip pins to the bottom |
frames | FilmStripHeroFrame[] | 8 placeholder scenes | Film frames; fewer than 10 are repeated to fill one copy |
speed | number | 40 (reads --wb-film-strip-hero-speed) | Base speed (px/s); reads the CSS variable when omitted |
direction | "left" | "right" | "left" | Direction the film travels |
velocityBoost | boolean | true | Speed up with page scroll velocity (up to 4× extra); scrolling up reverses it |
framesLabel | string | "Film frames" | Accessible name of the sr-only frame list (Chinese build default: "膠片影格", "Film frames") |
container | RefObject<HTMLElement> | — | Ref of the nested scroll container; scroll velocity is read from it too. Omit to use the window as the scroll container |
className | string | — | Merged into the root section |
FilmStripHeroFrame
| Field | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL; when omitted a warm gradient + geometric scene placeholder is drawn |
alt | string | — | Alt text; when provided it is also exposed through an sr-only list |
code | string | "01A", "02A"… | Edge code in the bottom-left corner |
FilmStripHeroAction
| Field | Type | Default | Description |
|---|---|---|---|
label | string | — | Button label |
href | string | — | Renders an a when provided, otherwise a type="button" |
onClick | () => void | — | Click callback |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-film-strip-hero-speed | 40 | Base speed (px/s; overridden when the speed prop is passed) |
--wb-film-strip-hero-duration | 600ms | Entrance duration for the title, copy, CTAs and film strip (80ms stagger) |
--wb-film-strip-hero-accent | #0d9488 (dark #2dd4bf) | Accent colour for the eyebrow and primary button |
--wb-hfs-hole | #ffffff (dark #0a0a0a) | Sprocket hole colour; set it to your background on non-white surfaces so the holes look punched through |
How it works
- The offset accumulates frame by frame with
useAnimationFrame; the content is duplicated once and the offset loops between minus one copy width and 0. Every cell width plus gap is a multiple of 24px, so the sprocket holes line up at the seam too. - Scroll velocity runs through
useScroll→useVelocity→useSpring(stiffness 400, damping 50) and maps to a 0–4 extra multiplier; scrolling up reverses the film until you scroll down again. - On hover the frame scales to 1.06 and the others drop to 60% opacity (200ms), while the whole strip springs down to 20% speed instead of stopping dead.
- Offset updates stop while the strip is off-screen; the CSS variable is re-read every 500ms, so speed changes from Theme Studio or global styles apply live.
Accessibility
- The root is a
sectionwitharia-labelledbypointing at the title; switch the heading toh2withasto avoid multipleh1s on a page - The film strip is decorative and fully
aria-hidden; whenframescarryalt, an sr-only list exposes the frame descriptions to screen readers - When keyboard focus lands on a CTA inside the section (focus-within), the film slows to 20% as well
- CTAs are
aortype="button"with a focus-visible ring - When the user has reduce motion enabled, the film stops looping and shows a static composition, scroll boost and hover enlarge are disabled, and the entrance becomes a 150ms fade
Drifting Logos Hero
A centred headline over a field of partner marks drifting slowly at different speeds and directions, with a subtle parallax that follows the pointer.
Image Tile Grid Hero
A hero with the headline on the left and a grid of image tiles on the right that enters outward from the centre, enlarging the hovered tile while the rest dim slightly.