liten

Beam Connect

An animated beam that draws between two nodes, a faint resting wire with a bright accent pulse travelling along it.

Beam Connect draws an animated beam between any two nodes, a faint resting wire with a bright pulse travelling along it, for integration maps, architecture and how-it-works diagrams. It measures both nodes against a shared container and re-measures on resize, so the beam stays locked as the layout reflows. Built with Motion.

Installation

Complete the shared Setup first, then add Motion.

Terminal
bun add motion

Copy the component into components/ui/beam-connect.tsx.

components/ui/beam-connect.tsx
'use client';import * as React from 'react';import { motion, useReducedMotion } from 'motion/react';export type BeamConnectProps = {  containerRef: React.RefObject<HTMLElement | null>;  fromRef: React.RefObject<HTMLElement | null>;  toRef: React.RefObject<HTMLElement | null>;  curvature?: number;  reverse?: boolean;  duration?: number;  delay?: number;  accent?: string;  thickness?: number;  startOffset?: { x?: number; y?: number };  endOffset?: { x?: number; y?: number };};export function BeamConnect({  containerRef,  fromRef,  toRef,  curvature = 0,  reverse = false,  duration = 3,  delay = 0,  accent = '#f0883e',  thickness = 1.6,  startOffset,  endOffset,}: BeamConnectProps) {  const reduce = useReducedMotion();  const id = React.useId().replace(/[:]/g, '');  const [box, setBox] = React.useState({ w: 0, h: 0 });  const [d, setD] = React.useState('');  React.useEffect(() => {    const container = containerRef.current;    const from = fromRef.current;    const to = toRef.current;    if (!container || !from || !to) return;    const measure = () => {      const cb = container.getBoundingClientRect();      const fb = from.getBoundingClientRect();      const tb = to.getBoundingClientRect();      setBox({ w: cb.width, h: cb.height });      const x1 = fb.left - cb.left + fb.width / 2 + (startOffset?.x ?? 0);      const y1 = fb.top - cb.top + fb.height / 2 + (startOffset?.y ?? 0);      const x2 = tb.left - cb.left + tb.width / 2 + (endOffset?.x ?? 0);      const y2 = tb.top - cb.top + tb.height / 2 + (endOffset?.y ?? 0);      const cx = (x1 + x2) / 2;      const cy = (y1 + y2) / 2 - curvature;      setD(`M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`);    };    measure();    const ro = new ResizeObserver(measure);    ro.observe(container);    ro.observe(from);    ro.observe(to);    window.addEventListener('resize', measure);    return () => {      ro.disconnect();      window.removeEventListener('resize', measure);    };  }, [containerRef, fromRef, toRef, curvature, startOffset, endOffset]);  const gradFrom = reverse    ? { x1: ['90%', '-10%'], x2: ['100%', '0%'] }    : { x1: ['-10%', '90%'], x2: ['0%', '100%'] };  return (    <svg      aria-hidden      width={box.w}      height={box.h}      viewBox={`0 0 ${box.w} ${box.h}`}      fill="none"      className="pointer-events-none absolute inset-0"    >      <path        d={d}        stroke="currentColor"        className="text-black/[0.12] dark:text-white/[0.12]"        strokeWidth={thickness}        strokeLinecap="round"      />      <path        d={d}        stroke={`url(#beam-${id})`}        strokeWidth={thickness}        strokeLinecap="round"      />      <defs>        <motion.linearGradient          id={`beam-${id}`}          gradientUnits="userSpaceOnUse"          initial={{ x1: '0%', x2: '0%', y1: '0%', y2: '0%' }}          animate={            reduce              ? { x1: '40%', x2: '60%' }              : { x1: gradFrom.x1, x2: gradFrom.x2 }          }          transition={            reduce              ? undefined              : {                  duration,                  delay,                  ease: [0.16, 1, 0.3, 1],                  repeat: Infinity,                  repeatDelay: 0,                }          }        >          <stop stopColor={accent} stopOpacity="0" />          <stop offset="0.5" stopColor={accent} />          <stop offset="1" stopColor={accent} stopOpacity="0" />        </motion.linearGradient>      </defs>    </svg>  );}

Usage

Give the three pieces refs: the container (must be position: relative) and the two nodes to connect. Render one <BeamConnect> per beam.

Example.tsx
import * as React from 'react';
import { BeamConnect } from '@/components/ui/beam-connect';

export default function Example() {
  const container = React.useRef<HTMLDivElement>(null);
  const from = React.useRef<HTMLDivElement>(null);
  const to = React.useRef<HTMLDivElement>(null);

  return (
    <div ref={container} className="relative flex justify-between">
      <div ref={from}>A</div>
      <div ref={to}>B</div>
      <BeamConnect containerRef={container} fromRef={from} toRef={to} />
    </div>
  );
}

Examples

Pass curvature to arc the beam and reverse to run the pulse the other way, stack two for a bidirectional link.

Props

PropTypeDefaultDescription
containerRefRefObject-Element the beam is measured inside (relative).
fromRefRefObject-Node the beam starts at.
toRefRefObject-Node the beam ends at.
curvaturenumber0Curve height of the path in px (0 = straight).
reversebooleanfalseRun the pulse from to to from.
durationnumber3Seconds for one pulse to travel.
delaynumber0Seconds before the first pulse.
accentstring"#f0883e"Color of the moving pulse.
thicknessnumber1.6Stroke width of the beam in px.
startOffset{ x?, y? }-Nudge the start point, in px.
endOffset{ x?, y? }-Nudge the end point, in px.
On this page0%