Display
Animated Beam
A resize-aware SVG connection that carries a token-colored highlight between referenced interface nodes.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/animated-beam.jsonPrompt
Build a React + TypeScript + Tailwind "AnimatedBeam" component with no motion
library.
Contract
- Export AnimatedBeamProps extending SVGAttributes<SVGSVGElement> except
children.
- Require containerRef, fromRef and toRef as
React.RefObject<HTMLElement | null>.
- Support curvature, reverse, duration, delay, pathWidth, beamLength,
startXOffset/startYOffset, endXOffset/endYOffset, trackClassName,
beamClassName, refreshKey, className and style. refreshKey is a string or
number-like React key that consumers change after position-only layout
updates such as a CSS transform.
- Defaults: curvature=0, reverse=false, duration=3.6, delay=0, pathWidth=2,
beamLength=0.22 and every offset=0.
Behavior
- Keep the server render deterministic: initialize to an empty geometry and
never call Math.random(), Date.now() or a browser API during render.
- After mount, re-read containerRef.current, fromRef.current and toRef.current
on every measurement; do not capture the initial nodes forever. Convert both
element centers into container-local coordinates and build a quadratic
Bézier path whose control point is the midpoint shifted by curvature.
- Observe the container and both endpoints with one ResizeObserver. Throttle
measurements into one requestAnimationFrame, synchronize observation if a
ref is retargeted, and also respond to window resize plus scroll in capture
phase so nested scrollers are covered. Changing refreshKey must schedule a
new measurement for CSS transforms or other position-only layout updates.
Cancel the frame, disconnect the observer and remove both listeners during
cleanup. Gracefully continue when ResizeObserver is unavailable.
- Normalize the animated path with pathLength="1". Clamp beamLength into a
useful fraction and move that dash using a deterministic CSS keyframe.
reverse changes animation-direction without swapping semantic endpoints.
- Clamp duration and delay to safe finite values.
Rendering & styling
- Render one absolute SVG with a quiet stroke-border track and a stroke-primary
traveling segment. Merge className, trackClassName and beamClassName via
cn(), and merge consumer style after internal CSS custom properties.
- Use semantic theme tokens only: no hex colors, palette-specific utilities,
fixed radii or shadows. Endpoint cards belong to the consumer, not the beam.
- Both paths use vector-effect="non-scaling-stroke" and round line caps so
responsive SVG scaling does not change apparent stroke weight.
- The SVG is decorative: aria-hidden=true, focusable=false and
pointer-events-none. The referenced nodes must carry all visible labels and
application semantics.
- Hide only the moving segment under prefers-reduced-motion; retain the quiet
route so the relationship stays understandable.
- Hoist the CSS keyframe through <style href precedence> so repeated beams
deduplicate and no Tailwind config change is needed.
Customization levers
- Shape: curvature changes the route without moving either endpoint; offsets
move individual anchors when a center attachment is not appropriate.
- Position-only updates: increment refreshKey after transforms or layout state
that moves a node without resizing it. Nested scrolling is detected
automatically through the capture-phase scroll listener.
- Tempo: duration and delay stagger multiple routes. Keep deterministic values
from component data instead of generating them during render.
- Signal density: beamLength controls how much of the normalized path is lit;
pathWidth controls both track and highlight weight.
- Theme: replace stroke-primary / stroke-border through beamClassName and
trackClassName using semantic tokens such as stroke-accent or
stroke-muted-foreground.
- Semantics: keep the SVG decorative. Put direction labels, status, keyboard
actions and accessible names on the real source/destination nodes.Concepts
- Ref-anchored geometry — the component does not own node layout. It measures consumer-owned elements and converts their centers into the coordinate system of one positioned container.
- Multi-signal measurement — one observer watches all three relevant boxes, captured scroll covers nested scrollers, and
refreshKeycovers transforms that change position without changing box size; an animation-frame throttle collapses every signal into one geometry update. - Normalized path motion —
pathLength="1"makes beam length and travel independent of the curve's pixel length, so short and long connections share one predictable animation contract. - Direction without topology changes —
reverseflips only the dash's animation direction; the source and destination refs remain stable, which prevents layout and measurement work during a flow toggle. - Decorative SVG semantics — the line explains a relationship visually but adds no standalone information to the accessibility tree; endpoint labels and application state stay on the actual nodes.
- Cleanup as part of behavior — the pending frame, resize observer and global listener are all released on unmount, so diagrams can appear inside dialogs or route transitions without leaving measurement work behind.