Display

Animated Beam

A resize-aware SVG connection that carries a token-colored highlight between referenced interface nodes.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

const KEYFRAMES = `@keyframes zab-travel{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}`

interface BeamGeometry {
  width: number
  height: number
  path: string
}

const EMPTY_GEOMETRY: BeamGeometry = { width: 0, height: 0, path: "" }

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/animated-beam.json

Prompt

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 refreshKey covers transforms that change position without changing box size; an animation-frame throttle collapses every signal into one geometry update.
  • Normalized path motionpathLength="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 changesreverse flips 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.

On This Page