All components

Element Along SVG Path

Element Along SVG Path is a React and Tailwind CSS component from Fancy Components, licensed MIT. Copy it and paste it into your project.

Fancy Components MIT other

What it is

Element Along SVG Path is a React and Tailwind CSS component from Fancy Components, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i motion

Licence

This component comes from Fancy Components and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Element Along SVG Path

fancy/blocks/element-along-svg-path.tsx
import {
  createContext,
  RefObject,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react"
import {
  motion,
  MotionValue,
  useMotionValue,
  useScroll,
  UseScrollOptions,
  useSpring,
  useTime,
  useTransform,
} from "motion/react"

import { cn } from "@/lib/utils"

type PreserveAspectRatioAlign =
  | "none"
  | "xMinYMin"
  | "xMidYMin"
  | "xMaxYMin"
  | "xMinYMid"
  | "xMidYMid"
  | "xMaxYMid"
  | "xMinYMax"
  | "xMidYMax"
  | "xMaxYMax"

type PreserveAspectRatioMeetOrSlice = "meet" | "slice"

type PreserveAspectRatio =
  | PreserveAspectRatioAlign
  | `${Exclude<PreserveAspectRatioAlign, "none">} ${PreserveAspectRatioMeetOrSlice}`

interface ElementAlongPathProps {
  // Path properties
  path: string
  pathId?: string
  className?: string
  preserveAspectRatio?: PreserveAspectRatio
  showPath?: boolean
  direction?: "normal" | "reverse"

  // SVG properties
  width?: string | number
  height?: string | number
  viewBox?: string

  // Animation properties
  animationType?: "auto" | "scroll"

  // Animation properties if animationType is auto
  duration?: number
  transition?: any

  // Scroll animation properties if animationType is scroll
  scrollContainer?: RefObject<HTMLElement | null>
  scrollOffset?: UseScrollOptions["offset"]
  scrollTransformValues?: [number, number]

  // Children
  children?: React.ReactNode
}

interface ElementAlongPathItemProps {
  children: React.ReactNode
  className?: string
  startOffset?: number // 0-100 percentage
  transition?: any // Override parent transition
}

// Create context
const ElementAlongPathContext = createContext<{
  path: string
  animationType: "auto" | "scroll"
  direction: "normal" | "reverse" // Add direction to context
  progress: MotionValue<number>
  scrollYProgress: MotionValue<number>
  scrollTransformValues: [number, number]
  transition: any
  setHovered: (isHovered: boolean) => void
} | null>(null)

// Context hook
export const useElementAlongPathContext = () => {
  const context = useContext(ElementAlongPathContext)
  if (!context) {
    throw new Error("ElementAlongPathItem must be used within ElementAlongPath")
  }
  return context
}

// Item component
export const ElementAlongPathItem = ({
  children,
  className,
  startOffset = 0,
  transition: itemTransition,
}: ElementAlongPathItemProps) => {
  const {
    path,
    animationType,
    progress,
    scrollYProgress,
    scrollTransformValues,
    direction,
    transition: parentTransition,
    setHovered,
  } = useElementAlongPathContext()

  // Use item transition if provided, otherwise use parent transition
  const transition = itemTransition || parentTransition

  const initialOffset =
    direction === "normal" ? `${startOffset}%` : `${100 - startOffset}%`
  const animateOffset = direction === "normal" ? "100%" : "0%"

  const scp = useTransform(
    scrollYProgress,
    [0, 1],
    [scrollTransformValues[0], scrollTransformValues[1]]
  )

  return (
    <motion.div
      className={cn("absolute top-0 left-0", className)}
      initial={{ offsetDistance: initialOffset }}
      animate={{
        offsetDistance: animationType === "auto" ? animateOffset : undefined,
      }}
      style={{
        offsetPath: `path('${path}')`,
        offsetDistance: animationType === "scroll" ? scp : undefined,
      }}
      transition={transition}
      // onHoverStart={() => setHovered(true)}
      // onHoverEnd={() => setHovered(false)}
    >
      {children}
    </motion.div>
  )
}

const ElementAlongPath = ({
  children,
  // Path defaults
  path,
  pathId,
  preserveAspectRatio = "xMidYMid meet",
  showPath = false,
  className,

  // SVG defaults
  width = "100%",
  height = "100%",
  viewBox = "0 0 100 100",

  // Animation type
  animationType = "auto",
  direction = "normal",

  // Animation defaults
  duration = 4,
  transition = { duration: 4, repeat: Infinity, ease: "linear" },

  // Scroll animation defaults
  scrollContainer,
  scrollOffset = ["start end", "end end"],
  scrollTransformValues = [0, 100],
}: ElementAlongPathProps) => {
  const container = useRef<HTMLDivElement>(null)
  const [isHovered, setIsHovered] = useState(false)

  // Create a time scale factor that changes based on hover state
  const timeScale = useMotionValue(1)

  // Update time scale when hover state changes
  useEffect(() => {
    timeScale.set(isHovered ? 0.3 : 1) // Slow down to 30% speed when hovered
  }, [isHovered, timeScale])

  // Create a spring-based time scale for smooth transitions
  const smoothTimeScale = useSpring(timeScale, {
    stiffness: 100,
    damping: 30,
  })

  const t = useTime()
  const scaledTime = useTransform(t, (time) => time * smoothTimeScale.get())

  const progress = useTransform(
    scaledTime,
    [0, duration],
    direction === "normal" ? [0, 100] : [100, 0]
  )
  // naive id for the path. you should rather use yours :)
  const id =
    pathId || `animated-path-${Math.random().toString(36).substring(7)}`

  const { scrollYProgress } = useScroll({
    container: scrollContainer || container,
    offset: scrollOffset,
  })

  // Adjust scroll progress based on direction
  const scrollProgressValues =
    direction === "normal"
      ? [scrollTransformValues[0], scrollTransformValues[1]]
      : [scrollTransformValues[1], scrollTransformValues[0]]

  const scrollProgress = useTransform(
    scrollYProgress,
    [0, 1],
    scrollProgressValues
  )

  // Create the progress value based on animation type
  const finalProgress = animationType === "auto" ? progress : scrollProgress

  return (
    <ElementAlongPathContext.Provider
      value={{
        path,
        animationType,
        direction,
        progress: finalProgress,
        scrollYProgress,
        scrollTransformValues,
        transition: {
          ...transition,
        },
        setHovered: setIsHovered,
      }}
    >
      <div
        ref={container}
        className={cn("relative", className)}
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox={viewBox}
          width={width}
          height={height}
          preserveAspectRatio={preserveAspectRatio}
          className="w-full h-full"
        >
          <motion.path
            id={id}
            d={path}
            // initial={{ pathLength: 0.001 }}
            // animate={{ pathLength: 1 }}
            stroke={showPath ? "currentColor" : "none"}
            fill="none"
            transition={transition}
          />
        </svg>
        {children}
      </div>
    </ElementAlongPathContext.Provider>
  )
}

export default ElementAlongPath

Similar components