Docs
Marquee Along SVG Path

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


  1. Wrap your elements with the MarqueeAlongSvgPath component
  2. Provide an SVG path via the required path prop (the d attribute of an SVG path)
  3. Configure the SVG viewport with optional viewBox and preserveAspectRatio props for proper scaling
  4. 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 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


PropTypeDefaultDescription
children*ReactNode-The elements to be scrolled along the path
path*string-The SVG path string that defines the motion path
pathIdstring-Optional ID for the SVG path element
preserveAspectRatiostring"xMidYMid meet"SVG preserveAspectRatio attribute value
showPathbooleanfalseWhether to show the SVG path
widthstring"100%"Width of the SVG container
heightstring"100%"Height of the SVG container
viewBoxstring"0 0 100 100"SVG viewBox attribute value
baseVelocitynumber5Base 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
slowdownOnHoverbooleanfalseWhether to slow down on hover
slowDownFactornumber0.3Factor to slow down by when hovering
slowDownSpringConfigSpringOptions{ damping: 50, stiffness: 400 }Spring configuration for hover slowdown
useScrollVelocitybooleanfalseWhether to use scroll velocity
scrollAwareDirectionbooleanfalseWhether to change direction based on scroll
scrollSpringConfigSpringOptions{ damping: 50, stiffness: 400 }Spring configuration for scroll velocity
scrollContainerRefObject<HTMLElement> | HTMLElement | null-Custom scroll container reference
repeatnumber3Number of times to repeat children
draggablebooleanfalseWhether elements can be dragged
dragSensitivitynumber0.2Sensitivity of drag movement
dragVelocityDecaynumber0.96Decay rate of drag velocity
dragAwareDirectionbooleanfalseWhether to change direction based on drag
grabCursorbooleanfalseWhether to show grab cursor when draggable
enableRollingZIndexbooleantrueWhether to enable rolling z-index effect
zIndexBasenumber1Base z-index value
zIndexRangenumber10Range of z-index values
cssVariableInterpolationArray<{ property: string, from: number, to: number }>[]CSS properties to interpolate along the path