Marquee Along SVG Path
A component that scrolls html elements along an SVG path.
Installation
npx shadcn@latest add "https://fancycomponents.dev/r/marquee-along-svg-path.json"
Usage
- Wrap your elements with the
MarqueeAlongSvgPath
component - Provide an SVG path via the required
path
prop (thed
attribute of an SVG path) - Configure the SVG viewport with optional
viewBox
andpreserveAspectRatio
props for proper scaling - The elements are distributed evenly along the path, so you'll need to experiment with:
- The
repeat
prop to control how many copies of your elements appear - The size of your elements (width/height)
- The
The component is really similar to the Simple Marquee Component, and has the same features and props (and a bit more:)):
- Changing velocity based on scroll velocity
- Slow down on hover
- Draggable elements
- Custom easing
Understanding the component
Before you dive into understanding this component, please read through the Simple Marquee component's documentation, as this one is almost identical.
The main difference is that we move the children along an SVG path (instead of a "straight line" positioned with flexbox
system, as in the other component). The magic that makes this possible is the offsetPath
CSS property.
The
offset-path
CSS property specifies a path for an element to follow and determines the element's positioning within the path's parent container or the SVG coordinate system. The path is a line, a curve, or a geometrical shape along which the element gets positioned or moves.
as per the offset-path documentation on MDN.
We also use the offsetDistance
property to actually move/offset the element to the correct position along the path in the offsetPath
CSS property.
style={{
...
offsetPath: `path('${path}')`,
offsetDistance: itemOffset,
}}
Each item's offset is calculated separately using an useTransform
hook from motion/react
, by converting the baseOffset
to a percentage value:
const itemOffset = useTransform(baseOffset, (v) => {
// evenly distribute items along the path (0-100%)
const position = (itemIndex * 100) / items.length
const wrappedValue = wrap(0, 100, v + position)
return `${easing ? easing(wrappedValue / 100) * 100 : wrappedValue}%`
})
The items are evenly distributed along the path. The wrap
function ensures that items surpassing 100%
are "wrapped back" to 0%
. The baseOffset
value (the input value for the useTransform
hook) is calculated by a bunch of different factors, such as:
- a base velocity, which moves the items along the path at a constant speed
- scroll velocity
- slowing down on hover
- direction
- drag velocity
Most of these factors are calculated inside an useAnimationFrame
hook, which runs every frame. Most of these values are either motion values or refs to avoid unnecessary re-renders. Please refer to the Simple Marquee Component documentation, there is a detailed explanation for each part.
Z-Index Management
You can enable increasing z-index based on the progress along the path by setting enableRollingZIndex
to true
. This is pretty useful when a path is self-crossing, so elements further along the path appear above earlier ones.
The callback function which calculates the current z-index is fairly simple. You can set the zIndexBase
and zIndexRange
props to control the base and range of the z-index values. The zIndexBase
is the starting value, and the zIndexRange
is the difference between the highest and lowest z-index values.
// Function to calculate z-index based on offset distance
const calculateZIndex = useCallback(
(offsetDistance: number) => {
if (!enableRollingZIndex) {
return undefined;
}
// Simple progress-based z-index
const normalizedDistance = offsetDistance / 100;
return Math.floor(zIndexBase + normalizedDistance * zIndexRange);
},
[enableRollingZIndex, zIndexBase, zIndexRange]
);
// ...
// Inside an element:
const zIndex = useTransform(
currentOffsetDistance,
(value) => calculateZIndex(value)
);
CSS Variable Interpolation
It's also possible to map any CSS property to the path progress using the cssVariableInterpolation
prop. It accepts an array of objects with property
and from
and to
values. High level example:
<MarqueeAlongSvgPath
path="M0,0 C0,0 100,0 100,100"
cssVariableInterpolation={[
{ property: "opacity", from: 0, to: 1.5 },
{ property: "scale", from: 0.1, to: 1 },
]}
>
{/* Your content */}
</MarqueeAlongSvgPath>
Notes
The component's performance may be impacted by the complexity and length of the SVG path, as well as the number of elements being animated. Keep an eye on it and tweak these factors if you experience performance issues.
Resources
Credits
Click on the individual images in the 2nd demo to see the original artworks & authors.
Props
Prop | Type | Default | Description |
---|---|---|---|
children* | ReactNode | - | The elements to be scrolled along the path |
path* | string | - | The SVG path string that defines the motion path |
pathId | string | - | Optional ID for the SVG path element |
preserveAspectRatio | string | "xMidYMid meet" | SVG preserveAspectRatio attribute value |
showPath | boolean | false | Whether to show the SVG path |
width | string | "100%" | Width of the SVG container |
height | string | "100%" | Height of the SVG container |
viewBox | string | "0 0 100 100" | SVG viewBox attribute value |
baseVelocity | number | 5 | Base velocity of the animation |
direction | "normal" | "reverse" | "normal" | Direction of the animation along the path |
easing | (value: number) => number | - | Custom easing function for the animation |
slowdownOnHover | boolean | false | Whether to slow down on hover |
slowDownFactor | number | 0.3 | Factor to slow down by when hovering |
slowDownSpringConfig | SpringOptions | { damping: 50, stiffness: 400 } | Spring configuration for hover slowdown |
useScrollVelocity | boolean | false | Whether to use scroll velocity |
scrollAwareDirection | boolean | false | Whether to change direction based on scroll |
scrollSpringConfig | SpringOptions | { damping: 50, stiffness: 400 } | Spring configuration for scroll velocity |
scrollContainer | RefObject<HTMLElement> | HTMLElement | null | - | Custom scroll container reference |
repeat | number | 3 | Number of times to repeat children |
draggable | boolean | false | Whether elements can be dragged |
dragSensitivity | number | 0.2 | Sensitivity of drag movement |
dragVelocityDecay | number | 0.96 | Decay rate of drag velocity |
dragAwareDirection | boolean | false | Whether to change direction based on drag |
grabCursor | boolean | false | Whether to show grab cursor when draggable |
enableRollingZIndex | boolean | true | Whether to enable rolling z-index effect |
zIndexBase | number | 1 | Base z-index value |
zIndexRange | number | 10 | Range of z-index values |
cssVariableInterpolation | Array<{ property: string, from: number, to: number }> | [] | CSS properties to interpolate along the path |