Page Sections

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

40
600
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
titleReactNode—Main title (required)
eyebrowReactNode—Small label above the title, in the accent colour
subtitleReactNode—Supporting copy below the title
primaryActionFilmStripHeroAction—Primary action with an accent-coloured background
secondaryActionFilmStripHeroAction—Outlined secondary action
as"h1" | "h2""h1"Heading level
minHeightnumber560Minimum section height (px); the film strip pins to the bottom
framesFilmStripHeroFrame[]8 placeholder scenesFilm frames; fewer than 10 are repeated to fill one copy
speednumber40 (reads --wb-film-strip-hero-speed)Base speed (px/s); reads the CSS variable when omitted
direction"left" | "right""left"Direction the film travels
velocityBoostbooleantrueSpeed up with page scroll velocity (up to 4× extra); scrolling up reverses it
framesLabelstring"Film frames"Accessible name of the sr-only frame list (Chinese build default: "膠片影格", "Film frames")
containerRefObject<HTMLElement>—Ref of the nested scroll container; scroll velocity is read from it too. Omit to use the window as the scroll container
classNamestring—Merged into the root section

FilmStripHeroFrame

FieldTypeDefaultDescription
srcstring—Image URL; when omitted a warm gradient + geometric scene placeholder is drawn
altstring—Alt text; when provided it is also exposed through an sr-only list
codestring"01A", "02A"…Edge code in the bottom-left corner

FilmStripHeroAction

FieldTypeDefaultDescription
labelstring—Button label
hrefstring—Renders an a when provided, otherwise a type="button"
onClick() => void—Click callback

CSS variables

VariableDefaultDescription
--wb-film-strip-hero-speed40Base speed (px/s; overridden when the speed prop is passed)
--wb-film-strip-hero-duration600msEntrance 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 section with aria-labelledby pointing at the title; switch the heading to h2 with as to avoid multiple h1s on a page
  • The film strip is decorative and fully aria-hidden; when frames carry alt, 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 a or type="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