GodUIGodUI
129Follow on X

Anatomy of Scroll Reveal

How useInView flips a spring from offset+blur to settled, and how scroll velocity optionally skews the plate.

01/04Direction is a unit vector
up
down
left
right
Offset
OFFSET[direction] × distance
Blur
blur(10px) → blur(0) when enabled
Plate
children wrapped in motion.div
tsx
const OFFSET = {up: { x: 0, y: 1 },down: { x: 0, y: -1 },left: { x: 1, y: 0 },right: { x: -1, y: 0 },};const hidden = reduceMotion? { opacity: 0 }: {    opacity: 0,    x: offset.x * distance,    y: offset.y * distance,    filter: blur ? "blur(10px)" : "blur(0px)",  };
01

Direction is a unit vector

ScrollReveal waits until 30% of its node is on screen, then springs from a directional offset (plus optional blur) to rest. Optionally, scroll velocity skews the plate on the Y axis. Reduced motion turns the skew off.

OFFSET maps each direction to { x, y } multipliers. Hidden state is offset * distance (default 40px) with opacity: 0 and, when blur is on, filter: blur(10px). Visible zeros the translate, clears the blur, and sets opacity to 1.

02/04useInView drives the spring

animate={isInView ? visible : hidden}

In view
useInView once · amount 0.3
Spring
damping 32 · stiffness 320 · mass 0.9
Hidden
opacity 0 + offset + blur(10px)
tsx
const isInView = useInView(ref, { once, amount: 0.3 });<motion.divinitial={hidden}animate={isInView ? visible : hidden}transition={{  type: "spring",  damping: 32,  stiffness: 320,  mass: 0.9,  delay,}}/>
02

useInView drives the spring

useInView(ref, { once, amount: 0.3 }) flips isInView. That boolean selects animate={isInView ? visible : hidden}. The spring is damping: 32, stiffness: 320, mass: 0.9, plus an optional delay for staggering siblings. With once (the default), the plate never re-hides after the first reveal.

03/04Velocity skew (optional)

useTransform(smoothVelocity, [-2000,0,2000], [6,0,-6])

Velocity
useVelocity(scrollY)
Spring
smoothVelocity · same 32/320/0.9
skewY
mapped to ±6° · clamp true
tsx
const scrollVelocity = useVelocity(scrollY);const smoothVelocity = useSpring(scrollVelocity, {damping: 32,stiffness: 320,mass: 0.9,});const skew = useTransform(smoothVelocity, [-2000, 0, 2000], [6, 0, -6], {clamp: true,});style={velocitySkew && !reduceMotion ? { skewY: skew, ...style } : style}
03

Velocity skew (optional)

When velocitySkew is on, useVelocity(scrollY) feeds the same spring config, then useTransform maps [-2000, 0, 2000] → [6, 0, -6] degrees of skewY (clamped). Fast scroll tips the plate; settling returns it to square. Reduced motion skips the skew, and only opacity animates.

04/04Result
04

The result

One intersection threshold, one spring recipe, and an optional velocity skew, all animated with compositor transforms.

Reduced motion

Under prefers-reduced-motion, hidden/visible collapse to opacity alone, with no translate, blur, or skew. The spring still runs, so the fade isn't instant, but the plate never slides or warps.

Motion Score

Scroll RevealSS: Compositor-only
SopacityFade / cross-fade
SfilterFilter (blur / brightness)
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →