Path Follow Scroll
A marker that rides an SVG curve on scroll, banking through the bends.
DOM + CSS1 dep1 file · 3 KB TSX
Props
"use client";
import { useRef } from "react";
import { motion, useScroll, useSpring, useTransform } from "framer-motion";
export function PathFollowScroll({
d,
viewBox = "0 0 340 170",
markerSize = 14,
drawPath = true,
autoRotate = true,
}: {
/** Any SVG path string. The marker rides this exact curve. */
d: string;
viewBox?: string;
markerSize?: number;
/** Draw the trail in behind the marker as it travels. */
drawPath?: boolean;
/** Let the browser bank the marker to the curve's tangent. */
autoRotate?: boolean;
}) {
const sectionRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const progress = useSpring(scrollYProgress, { stiffness: 220, damping: 40 });
const offsetDistance = useTransform(progress, (v) => v * 100 + "%");
// Hoisted: feeding this inline into JSX that may be conditional would make
// a hook run conditionally.
const dashOffset = useTransform(progress, (v) => 1 - v);
return (
<div ref={sectionRef} className="relative h-[200vh]">
<div className="sticky top-1/3">
<div className="relative mx-auto w-full max-w-3xl">
<svg aria-hidden viewBox={viewBox} className="w-full overflow-visible">
<path d={d} fill="none" stroke="currentColor" strokeOpacity={0.14} strokeWidth={1.5} />
{drawPath && (
<motion.path
d={d}
fill="none"
stroke="#3d7bff"
strokeWidth={1.5}
strokeLinecap="round"
/*
* pathLength={1} normalises the path so dasharray/dashoffset are
* plain 0-1 fractions — no getTotalLength(), and it keeps working
* at any rendered size.
*/
pathLength={1}
strokeDasharray="1 1"
style={{ strokeDashoffset: dashOffset }}
/>
)}
</svg>
{/*
* The marker rides CSS offset-path / offset-distance rather than
* interpolated x/y. The browser solves position AND tangent angle
* along the curve, so offsetRotate:"auto" banks it through the bends
* for free — doing that manually means sampling and differentiating
* the path yourself.
*/}
<motion.div
aria-hidden
className="absolute left-0 top-0 rounded-full bg-blue-500"
style={{
width: markerSize,
height: markerSize,
offsetPath: 'path("' + d + '")',
offsetDistance,
offsetRotate: autoRotate ? "auto" : "0deg",
}}
/>
</div>
</div>
</div>
);
}
Free. Pro unlocks everything, from $27/month.
Details
About this animation
Moving an element along a curve is usually written as manual interpolation: sample the path, store points, lerp x and y between them, and — if you want the element to face where it is going — differentiate the samples to get a tangent. CSS `offset-path` makes all of that unnecessary. Set the path, drive `offset-distance` from zero to a hundred percent, and the browser solves position and tangent together, so `offset-rotate: auto` banks the marker through every bend at no cost. The trail behind it uses the same `pathLength` normalisation trick as this library's Stroke Draw Button: setting it to one turns dash values into plain fractions, which removes the `getTotalLength` call and keeps the drawing correct at any rendered size. Scroll progress is tied to the section's own passage through the viewport and smoothed through a spring, so a jumpy trackpad does not translate into a jittery marker. One rule that bites here more than elsewhere: hoist every `useTransform` to the top level — feeding one inline into JSX that renders conditionally makes a hook run conditionally.
Curator’s note
Built for viberdy 2.0. offset-path does the tangent maths the hard version of this component spends fifty lines on.


