Animated Beam

An animated beam of light traveling along a path, perfect for showcasing website integration features

Installation

npm install framer-motion
beam.tsx
'use client'
 
import { cn } from '@/lib/utils'
import { motion } from 'framer-motion'
import { forwardRef, RefObject, useEffect, useId, useState } from 'react'
 
export interface AnimatedBeamProps {
  className?: string
  containerRef: RefObject<HTMLElement> // Container ref
  fromRef: RefObject<HTMLElement>
  toRef: RefObject<HTMLElement>
  curvature?: number
  reverse?: boolean
  pathColor?: string
  pathWidth?: number
  pathOpacity?: number
  gradientStartColor?: string
  gradientStopColor?: string
  delay?: number
  duration?: number
  startXOffset?: number
  startYOffset?: number
  endXOffset?: number
  endYOffset?: number
  dotted?: boolean
  dotSpacing?: number
}
 
export const AnimatedBeam: React.FC<AnimatedBeamProps> = ({
  className,
  containerRef,
  fromRef,
  toRef,
  curvature = 0,
  reverse = false, // Include the reverse prop
  duration = Math.random() * 3 + 4,
  delay = 0,
  pathColor = 'gray',
  pathWidth = 2,
  pathOpacity = 0.2,
  gradientStartColor = '#4d40ff',
  gradientStopColor = '#4043ff',
  startXOffset = 0,
  startYOffset = 0,
  endXOffset = 0,
  endYOffset = 0,
  dotted = false,
  dotSpacing = 6,
}) => {
  const id = useId()
  const [pathD, setPathD] = useState('')
 
  const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 })
  const strokeDasharray = dotted ? `${dotSpacing} ${dotSpacing}` : 'none'
  // Calculate the gradient coordinates based on the reverse prop
  const gradientCoordinates = reverse
    ? {
        x1: ['90%', '-10%'],
        x2: ['100%', '0%'],
        y1: ['0%', '0%'],
        y2: ['0%', '0%'],
      }
    : {
        x1: ['10%', '110%'],
        x2: ['0%', '100%'],
        y1: ['0%', '0%'],
        y2: ['0%', '0%'],
      }
 
  useEffect(() => {
    const updatePath = () => {
      if (containerRef.current && fromRef.current && toRef.current) {
        const containerRect = containerRef.current.getBoundingClientRect()
        const rectA = fromRef.current.getBoundingClientRect()
        const rectB = toRef.current.getBoundingClientRect()
 
        const svgWidth = containerRect.width
        const svgHeight = containerRect.height
        setSvgDimensions({ width: svgWidth, height: svgHeight })
 
        const startX =
          rectA.left - containerRect.left + rectA.width / 2 + startXOffset
        const startY =
          rectA.top - containerRect.top + rectA.height / 2 + startYOffset
        const endX =
          rectB.left - containerRect.left + rectB.width / 2 + endXOffset
        const endY =
          rectB.top - containerRect.top + rectB.height / 2 + endYOffset
 
        const controlY = startY - curvature
        const d = `M ${startX},${startY} Q ${
          (startX + endX) / 2
        },${controlY} ${endX},${endY}`
        setPathD(d)
      }
    }
 
    // Initialize ResizeObserver
    const resizeObserver = new ResizeObserver((entries) => {
      // For all entries, recalculate the path
      for (let entry of entries) {
        updatePath()
      }
    })
 
    // Observe the container element
    if (containerRef.current) {
      resizeObserver.observe(containerRef.current)
    }
 
    // Call the updatePath initially to set the initial path
    updatePath()
 
    // Clean up the observer on component unmount
    return () => {
      resizeObserver.disconnect()
    }
  }, [
    containerRef,
    fromRef,
    toRef,
    curvature,
    startXOffset,
    startYOffset,
    endXOffset,
    endYOffset,
  ])
 
  return (
    <svg
      fill="none"
      width={svgDimensions.width}
      height={svgDimensions.height}
      xmlns="http://www.w3.org/2000/svg"
      className={cn(
        'pointer-events-none absolute left-0 top-0 transform-gpu stroke-2',
        className
      )}
      viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}
    >
      <path
        d={pathD}
        stroke={pathColor}
        strokeWidth={pathWidth}
        strokeOpacity={pathOpacity}
        strokeLinecap="round"
        strokeDasharray={strokeDasharray}
      />
      <motion.path
        d={pathD}
        stroke={`url(#${id})`}
        strokeLinecap="round"
        strokeDasharray={strokeDasharray}
        initial={{
          strokeWidth: pathWidth,
          strokeOpacity: 0,
        }}
        animate={{
          strokeWidth: pathWidth * 1.5, // or any scale factor you prefer
          strokeOpacity: 1,
        }}
        transition={{
          duration: 2, // adjust as needed
          delay: delay, // use the same delay as the gradient animation
        }}
      />
      <defs>
        <motion.linearGradient
          className="transform-gpu"
          id={id}
          gradientUnits={'userSpaceOnUse'}
          initial={{
            x1: '0%',
            x2: '0%',
            y1: '0%',
            y2: '0%',
          }}
          animate={{
            x1: gradientCoordinates.x1,
            x2: gradientCoordinates.x2,
            y1: gradientCoordinates.y1,
            y2: gradientCoordinates.y2,
          }}
          transition={{
            delay,
            duration,
            ease: [0.16, 1, 0.3, 1], // https://easings.net/#easeOutExpo
            repeat: Infinity,
            repeatDelay: 0,
          }}
        >
          <stop stopColor={gradientStartColor} stopOpacity="0"></stop>
          <stop stopColor={gradientStartColor}></stop>
          <stop offset="32.5%" stopColor={gradientStopColor}></stop>
          <stop
            offset="100%"
            stopColor={gradientStopColor}
            stopOpacity="0"
          ></stop>
        </motion.linearGradient>
      </defs>
    </svg>
  )
}
 
export const Circle = forwardRef<
  HTMLDivElement,
  { className?: string; children?: React.ReactNode }
>(({ className, children }, ref) => {
  return (
    <div
      ref={ref}
      className={cn(
        'z-10 flex h-12 w-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]',
        className
      )}
    >
      {children}
    </div>
  )
})

Example

Bidrectional Beam

Unidrectional Beam

Multiple Input Beam

Multiple Output Beam

Props

PropTypeDescriptionDefault
classNamestringThe class name for the SVG element.-
containerRefRefObjectThe ref for the container element.-
fromRefRefObjectThe ref of the element from which the beam should start.-
toRefRefObjectThe ref of the element to which the beam should end.-
curvaturenumberThe curvature of the beam.0
reversebooleanWhether the beam should be reversed.false
durationnumberThe duration of the beam animation.Random (4-7)
delaynumberThe delay before the beam animation starts.0
pathColorstringThe color of the beam path."gray"
pathWidthnumberThe width of the beam path.2
pathOpacitynumberThe opacity of the beam path.0.2
gradientStartColorstringThe start color of the gradient for the beam."#4d40ff"
gradientStopColorstringThe stop color of the gradient for the beam."#4043ff"
startXOffsetnumberThe x offset of the beam's start position.0
startYOffsetnumberThe y offset of the beam's start position.0
endXOffsetnumberThe x offset of the beam's end position.0
endYOffsetnumberThe y offset of the beam's end position.0
dottedbooleanWhether the beam should be dotted.false
dotSpacingnumberThe spacing between dots if the beam is dotted.6