Cinematic hero
Split headline rising on a beat, pointer-parallax glow, a marquee strip, and the whole block fades out as you scroll past (--sv-t).
Studio · 2026 reel
One scroll listener, one frame, and CSS does the rest. This hero is 40 lines of CSS on top of the presets.
See the work
Brand·Motion·Web·Type·
// ScrollVars fx · hero-cinematic
// Requires: npm i scrollvars · import 'scrollvars/styles/core.css' and 'scrollvars/styles/ui.css'
// Split headline rising on a beat, pointer-parallax glow, marquee strip; the block
// fades and scales out as it leaves (--sv-t). The CSS below is the whole section,
// one constant string injected as raw HTML: React 18's server renderer escapes a
// <style> child (`>` becomes `>`) and <style> is raw text, so the entity never
// decodes and every child-combinator rule is dropped. CSP is unchanged from any
// inline <style>: a style-src nonce or hash, or lift the string into your own CSS.
'use client'
import * as React from 'react'
import { Track, Split, Marquee, usePointer } from 'scrollvars/react'
const css = `
.sv-hero { position: relative; min-height: 100svh; display: grid; place-items: center; overflow: hidden; isolation: isolate; text-align: center; }
.sv-hero .hero-orb { position: absolute; width: 52vmin; height: 52vmin; border-radius: 50%; filter: blur(70px); opacity: .5; z-index: -1;
translate: calc(var(--mx, 0) * var(--hero-parallax, 40px)) calc(var(--my, 0) * var(--hero-parallax, 40px)); transition: translate .5s ease-out; }
.sv-hero .hero-orb.a { background: var(--hero-glow, #f2c14e); top: -14%; left: -8%; }
.sv-hero .hero-orb.b { background: var(--hero-glow-2, #ffb454); bottom: -16%; right: -10%; --hero-parallax: -60px; }
.sv-hero .hero-inner { padding: 60px 24px 90px; --hero-out: clamp(0, (var(--sv-t, .5) - .5) * 2, 1); opacity: calc(1 - var(--hero-out)); scale: calc(1 - var(--hero-out) * .12); }
@media (prefers-reduced-motion: reduce) { .sv-hero .hero-orb { translate: none; transition: none; } .sv-hero .hero-inner { opacity: 1; scale: none; } }
/* the same under html[data-sv-motion="reduce"], the site's own switch */
:where([data-sv-motion="reduce"]) .sv-hero .hero-orb { translate: none; transition: none; }
:where([data-sv-motion="reduce"]) .sv-hero .hero-inner { opacity: 1; scale: none; }
.sv-hero .hero-eyebrow { font-size: 12px; letter-spacing: .18em; text-transform: uppercase; opacity: .8; }
.sv-hero .hero-title { font-size: clamp(36px, 6.4vw, 78px); line-height: 1.02; letter-spacing: -.03em; max-width: 14ch; margin: 14px auto 18px; font-weight: 800; }
.sv-hero .hero-sub { max-width: 42ch; margin: 0 auto 26px; font-size: 17px; opacity: .75; }
.sv-hero .hero-strip { position: absolute; left: 0; right: 0; bottom: 0; background: rgba(20, 33, 26, .9); padding: 14px 0; border-top: 1px solid rgba(128,128,128,.25); font-size: 13px; letter-spacing: .12em; text-transform: uppercase; opacity: .7; }
.sv-hero .hero-strip span { margin: 0 18px; }
`
export function HeroCinematic({
eyebrow,
title,
copy,
cta,
strip = ['Brand', '·', 'Motion', '·', 'Web', '·', 'Type', '·'],
className,
nonce,
}: {
eyebrow?: string
/** Plain text: it is split into words on the server, each rising on its own beat. */
title: string
copy?: React.ReactNode
cta?: React.ReactNode
strip?: string[]
className?: string
nonce?: string
}) {
const ref = usePointer<HTMLElement>({ selector: '.sv-hero' }) // --mx/--my (-1..1) on the section itself
return (
<section ref={ref} className={className ? 'sv-hero ' + className : 'sv-hero'}>
<style nonce={nonce} dangerouslySetInnerHTML={{ __html: css }} />
<div className="hero-orb a" />
<div className="hero-orb b" />
<Track travel>
<div className="hero-inner">
{eyebrow && <p className="hero-eyebrow sv-rise">{eyebrow}</p>}
<Split as="h1" className="hero-title sv-split-rise">
{title}
</Split>
{copy && (
<p className="hero-sub sv-rise" style={{ '--sv-order': 6 } as React.CSSProperties}>
{copy}
</p>
)}
{cta && (
<p className="sv-rise" style={{ '--sv-order': 7 } as React.CSSProperties}>
{cta}
</p>
)}
</div>
</Track>
<Marquee className="hero-strip">
{strip.map((s, i) => (
<span key={i}>{s}</span>
))}
</Marquee>
</section>
)
}