shadimage
jolyui
uianimated-beamsource: ok · refreshed

Animated Beam

npx shadcn add "https://www.jolyui.dev/r/animated-beam.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Files1 · source cached 10/8/2026, 7:44:09 AM

ui/animated-beam.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

interface AnimatedBeamProps {
  /** Reference to the container element */
  containerRef: React.RefObject<HTMLElement | null>;
  /** Reference to the start element */
  fromRef: React.RefObject<HTMLElement | null>;
  /** Reference to the end element */
  toRef: React.RefObject<HTMLElement | null>;
  /** Curvature of the beam (-1 to 1, 0 is straight) */
  curvature?: number;
  /** Animation duration in seconds */
  duration?: number;
  /** Delay before animation starts */
  delay?: number;
  /** Reverse the animation direction */
  reverse?: boolean;
  /** Width of the beam path */
  pathWidth?: number;
  /** Color of the beam gradient start */
  gradientStartColor?: string;
  /** Color of the beam gradient end */
  gradientStopColor?: string;
  /** Starting point offset */
  startXOffset?: number;
  startYOffset?: number;
  /** Ending point offset */
  endXOffset?: number;
  endYOffset?: number;
  className?: string;
}

const AnimatedBeam = ({
  containerRef,
  fromRef,
  toRef,
  curvature = 0,
  duration = 2,
  delay = 0,
  reverse = false,
  pathWidth = 2,
  gradientStartColor = "#18181b",
  gradientStopColor = "#18181b",
  startXOffset = 0,
  startYOffset = 0,
  endXOffset = 0,
  endYOffset = 0,
  className,
}: AnimatedBeamProps) => {
  const [pathD, setPathD] = React.useState("");
  const [svgDimensions, setSvgDimensions] = React.useState({
    width: 0,
    height: 0,
  });
  const uniqueId = React.useId();

  const updatePath = React.useCallback(() => {
    if (!containerRef.current || !fromRef.current || !toRef.current) return;

    const containerRect = containerRef.current.getBoundingClientRect();
    const fromRect = fromRef.current.getBoundingClientRect();
    const toRect = toRef.current.getBoundingClientRect();

    const startX =
      fromRect.left - containerRect.left + fromRect.width / 2 + startXOffset;
    const startY =
      fromRect.top - containerRect.top + fromRect.height / 2 + startYOffset;
    const endX =
      toRect.left - containerRect.left + toRect.width / 2 + endXOffset;
    const endY =
      toRect.top - containerRect.top + toRect.height / 2 + endYOffset;

    const midX = (startX + endX) / 2;
    const midY = (startY + endY) / 2;

    // Calculate control point for quadratic bezier curve
    const dx = endX - startX;
    const dy = endY - startY;
    const controlX = midX - dy * curvature;
    const controlY = midY + dx * curvature;

    const path = `M ${startX},${startY} Q ${controlX},${controlY} ${endX},${endY}`;
    setPathD(path);
    setSvgDimensions({
      width: containerRect.width,
      height: containerRect.height,
    });
  }, [
    containerRef,
    fromRef,
    toRef,
    curvature,
    startXOffset,
    startYOffset,
    endXOffset,
    endYOffset,
  ]);

  React.useEffect(() => {
    updatePath();

    const resizeObserver = new ResizeObserver(updatePath);
    if (containerRef.current) {
      resizeObserver.observe(containerRef.current);
    }

    window.addEventListener("resize", updatePath);

    return () => {
      resizeObserver.disconnect();
      window.removeEventListener("resize", updatePath);
    };
  }, [updatePath, containerRef]);

  return (
    <svg
      className={cn(
        "pointer-events-none absolute top-0 left-0 h-full w-full",
        className,
      )}
      width={svgDimensions.width}
      height={svgDimensions.height}
      viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <defs>
        {/* Background path gradient */}
        <linearGradient
          id={`beam-gradient-bg-${uniqueId}`}
          gradientUnits="userSpaceOnUse"
          x1="0%"
          y1="0%"
          x2="100%"
          y2="0%"
        >
          <stop offset="0%" stopColor={gradientStartColor} stopOpacity="0.1" />
          <stop offset="50%" stopColor={gradientStartColor} stopOpacity="0.2" />
          <stop offset="100%" stopColor={gradientStopColor} stopOpacity="0.1" />
        </linearGradient>

        {/* Animated beam gradient */}
        <linearGradient
          id={`beam-gradient-${uniqueId}`}
          gradientUnits="userSpaceOnUse"
          x1="0%"
          y1="0%"
          x2="100%"
          y2="0%"
        >
          <stop offset="0%" stopColor={gradientStartColor} stopOpacity="0" />
          <stop offset="5%" stopColor={gradientStartColor} stopOpacity="1" />
          <stop offset="50%" stopColor={gradientStopColor} stopOpacity="1" />
          <stop offset="95%" stopColor={gradientStopColor} stopOpacity="1" />
          <stop offset="100%" stopColor={gradientStopColor} stopOpacity="0" />
        </linearGradient>

        {/* Glow filter */}
        <filter
          id={`beam-glow-${uniqueId}`}
          x="-50%"
          y="-50%"
          width="200%"
          height="200%"
        >
          <feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur" />
          <feMerge>
            <feMergeNode in="blur" />
            <feMergeNode in="SourceGraphic" />
          </feMerge>
        </filter>

        {/* Mask for animated beam */}
        <mask id={`beam-mask-${uniqueId}`}>
          <rect
            className="beam-mask-rect"
            x="-100%"
            y="0"
            width="50%"
            height="100%"
            fill="url(#beam-mask-gradient)"
            style={{
              animation: `beam-flow ${duration}s linear infinite`,
              animationDelay: `${delay}s`,
              animationDirection: reverse ? "reverse" : "normal",
            }}
          />
        </mask>

        <linearGradient
          id="beam-mask-gradient"
          x1="0%"
          y1="0%"
          x2="100%"
          y2="0%"
        >
          <stop offset="0%" stopColor="black" />
          <stop offset="25%" stopColor="white" />
          <stop offset="75%" stopColor="white" />
          <stop offset="100%" stopColor="black" />
        </linearGradient>
      </defs>

      {/* Background path */}
      <path
        d={pathD}
        stroke={`url(#beam-gradient-bg-${uniqueId})`}
        strokeWidth={pathWidth}
        strokeLinecap="round"
        fill="none"
      />

      {/* Animated glowing beam */}
      <path
        d={pathD}
        stroke={`url(#beam-gradient-${uniqueId})`}
        strokeWidth={pathWidth}
        strokeLinecap="round"
        fill="none"
        filter={`url(#beam-glow-${uniqueId})`}
        className="animated-beam-path"
        style={{
          strokeDasharray: "20 1000",
          strokeDashoffset: reverse ? "-1000" : "1000",
          animation: `beam-dash ${duration}s linear infinite`,
          animationDelay: `${delay}s`,
          animationDirection: reverse ? "reverse" : "normal",
        }}
      />
    </svg>
  );
};

interface BeamContainerProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode;
}

const BeamContainer = React.forwardRef<HTMLDivElement, BeamContainerProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <div ref={ref} className={cn("relative", className)} {...props}>
        {children}
      </div>
    );
  },
);
BeamContainer.displayName = "BeamContainer";

interface BeamNodeProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode;
}

const BeamNode = React.forwardRef<HTMLDivElement, BeamNodeProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          "relative z-10 flex items-center justify-center rounded-xl border bg-background p-3 shadow-sm",
          className,
        )}
        {...props}
      >
        {children}
      </div>
    );
  },
);
BeamNode.displayName = "BeamNode";

export {
  AnimatedBeam,
  BeamContainer,
  BeamNode,
  type AnimatedBeamProps,
  type BeamContainerProps,
  type BeamNodeProps,
};

More from jolyui

Browse all components in this library →