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.
bun add motionCopy the component into 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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
containerRef | RefObject | - | Element the beam is measured inside (relative). |
fromRef | RefObject | - | Node the beam starts at. |
toRef | RefObject | - | Node the beam ends at. |
curvature | number | 0 | Curve height of the path in px (0 = straight). |
reverse | boolean | false | Run the pulse from to to from. |
duration | number | 3 | Seconds for one pulse to travel. |
delay | number | 0 | Seconds before the first pulse. |
accent | string | "#f0883e" | Color of the moving pulse. |
thickness | number | 1.6 | Stroke width of the beam in px. |
startOffset | { x?, y? } | - | Nudge the start point, in px. |
endOffset | { x?, y? } | - | Nudge the end point, in px. |