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 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.
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 entirely — only opacity animates.
The result
One intersection threshold, one spring recipe, and an optional velocity skew — compositor transforms from the first pixel to the last.
- 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 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.
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 entirely — only opacity animates.
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
opacityFade / cross-fadefilterFilter (blur / brightness)