Motion UIComponentsComponent

Screenshot scroll reveal

A headline that lifts away while a screenshot tilts upright and scales into centre on scroll.

npx shadcn@latest add @motion/screenshot-scroll-reveal

API reference

Components

<ScreenshotScrollReveal />

The canonical take-over stage: a <section> that grows past the viewport (so the page scrolls the reveal), pins a sticky frame, and lays the headline and screenshot slots out as two independent full-frame layers driven by useScreenshotScrollReveal. Under reduced motion it collapses to one static composed frame - headline above, screenshot below, centred as a group.

headline

ReactNode

The headline block - rendered centred in the headline layer (and in the reduced-motion static frame). It should contain the element referenced by ariaLabelledBy, so the section is labelled by its own heading.

screenshot

ReactNode

The screenshot block - your product screenshot, coded dashboard or video still. Rendered in the screenshot layer (and in the static frame).

ariaLabelledBy?

string

The id of the heading inside headline, wired to the section's aria-labelledby so the region is named by its own headline.

className?

string

Merged onto the <section> root, after the base surface classes.

Hooks

useScreenshotScrollReveal

The take-over mechanic as a hook: owns useScroll on a section ref and derives the two layers' scroll-linked motion styles plus their entrance props, all timed by the theme and shaped by the speed controls. Attach sectionRef to your scrolling section, branch on reduce for the static frame, and spread screenshotStyle/headlineTransformStyle/ headlineOpacityStyle onto your own layers. ScreenshotScrollReveal wires this into the canonical two-layer stage for you.

useScreenshotScrollReveal(options: ScreenshotScrollRevealOptions): ScreenshotScrollRevealState

tiltSpeed?

number = 1

How hard the screenshot starts tilted back (rotateX degrees = base * this). 0 starts the screenshot already flat, so straightening becomes pure scale. Defaults to 1.

scaleSpeed?

number = 1

How much smaller the screenshot starts, and how far below centre it rests. 0 starts the screenshot already at full size, in place. Defaults to 1.

headlineSpeed?

number = 1

How far the headline releases upward as it fades. 0 fades it in place. Defaults to 1.

Returns

sectionRef

RefObject<HTMLElement | null>

Attach to the scrolling section - useScroll measures the take-over against this element's travel through the viewport.

reduce

boolean

true when either reduced-motion strategy is active: render the single static composed frame instead of the pinned reveal.

still

boolean

The "off" strategy: mount no entrance animation at all.

calm

boolean

The "calm" strategy: keep the entrance fade but drop its travel.

screenshotStyle

MotionStyle | undefined

The screenshot layer's scroll-linked style (transform plus per-element perspective). undefined under reduced motion.

headlineTransformStyle

MotionStyle | undefined

The headline layer's scroll-linked transform and filter. undefined under reduced motion.

headlineOpacityStyle

MotionStyle | undefined

The headline layer's scroll-linked opacity. Kept on its own node, separate from transform/filter, so scroll opacity stays live (mixing opacity with transform MotionValues on one node freezes opacity at mount). Function form here because array keyframes on a child of an entrance-animated parent do not bind to the scroll timeline reliably yet. undefined under reduced motion.

headlineInitial

false | { opacity: number; transform: string }

The headline layer's entrance initial (false under the "off" strategy).

screenshotInitial

false | { opacity: number; transform: string }

The screenshot layer's entrance initial (false under the "off" strategy).

enterTransition

UITransition

The shared entrance transition (the theme's gentle token).

screenshotDelay

number

The screenshot layer's entrance delay (the theme's base stagger), so the screenshot settles just after the headline.