Anatomy of Scroll Reveal
How useInView flips a spring from offset+blur to settled, and how scroll velocity optionally skews the plate.
↓Scroll to step through it
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.
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.
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.
The result
One intersection threshold, one spring recipe, and an optional velocity skew, all animated with compositor transforms.
- Offset
- OFFSET[direction] × distance
- Blur
- blur(10px) → blur(0) when enabled
- Plate
- children wrapped in motion.div
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)", };Anatomy of Scroll Reveal
How useInView flips a spring from offset+blur to settled, and how scroll velocity optionally skews the plate.
- Offset
- OFFSET[direction] × distance
- Blur
- blur(10px) → blur(0) when enabled
- Plate
- children wrapped in motion.div
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)", };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.
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)
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,}}/>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.
useTransform(smoothVelocity, [-2000,0,2000], [6,0,-6])
- Velocity
- useVelocity(scrollY)
- Spring
- smoothVelocity · same 32/320/0.9
- skewY
- mapped to ±6° · clamp true
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}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.
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
opacityFade / cross-fadefilterFilter (blur / brightness)