MynduiMyndui
0

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 for a fluid, reactive feel — disabled entirely under reduced motion.

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 entirely — only opacity animates.

04/04Result
04

The result

One intersection threshold, one spring recipe, and an optional velocity skew — compositor transforms from the first pixel to the last.

Reduced motion

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

Motion Score

Scroll RevealSSCompositor-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 →