ScrollVars / fx
docs · demo · bench · llms.txt · GitHub

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

Use it for: Landing pages, studio reels, product launches. The first fold that has to land.
Knobs: --sv-stagger (word beat), --hero-parallax (px of pointer drift), --sv-marquee-duration; swap the orbs for images or video

Studio · 2026 reel

Sites that move with intent

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·

Install: npx scrollvars add hero-cinematic
Styles: import 'scrollvars/styles/core.css' · import 'scrollvars/styles/ui.css'

// 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 `&gt;`) 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>
  )
}

Engine: npm i scrollvars, 12.9 KB gzip as ESM (this page's fx/sv.js IIFE: 13.1 KB). All effects respect prefers-reduced-motion and render complete without JS.