Skip to content

Animated Beam

A light that travels along a curved line between two elements, such as two cards in a diagram. The line is drawn on a canvas that fills the container, so nothing is added to the accessibility tree.

Motionv0.1.0experimentalWCAG 2.2 AAView spec

Optional, and kept apart.

Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.

import { forwardRef, useRef, type ReactNode } from "react";
import { AnimatedBeam, ChartIcon, CreditCardIcon, InboxIcon, SparkleIcon, UsersIcon, FileIcon } from "@rdloom/react";

const Node = forwardRef<HTMLDivElement, { children: ReactNode; big?: boolean }>(function Node({ children, big }, ref) {
  return (
    <div
      ref={ref}
      className={`relative z-10 flex items-center justify-center rounded-full border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] text-[var(--rd-color-text-default)] [box-shadow:var(--rd-elevation-raised)] ${
        big ? "size-16" : "size-12"
      }`}
    >
      {children}
    </div>
  );
});

// Several sources feed one hub, and the hub feeds two places. Each beam is a decorative line between two real elements.
export default function AnimatedBeamBasicExample() {
  const container = useRef<HTMLDivElement>(null);
  const orders = useRef<HTMLDivElement>(null);
  const customers = useRef<HTMLDivElement>(null);
  const invoices = useRef<HTMLDivElement>(null);
  const hub = useRef<HTMLDivElement>(null);
  const reports = useRef<HTMLDivElement>(null);
  const inbox = useRef<HTMLDivElement>(null);
  return (
    <div className="flex w-full justify-center">
      <div ref={container} className="relative flex h-80 w-full max-w-2xl items-center justify-between px-6">
        <div className="flex flex-col gap-8">
          <Node ref={orders}>
            <FileIcon className="size-5" />
          </Node>
          <Node ref={customers}>
            <UsersIcon className="size-5" />
          </Node>
          <Node ref={invoices}>
            <CreditCardIcon className="size-5" />
          </Node>
        </div>
        <Node ref={hub} big>
          <SparkleIcon className="size-7" />
        </Node>
        <div className="flex flex-col gap-16">
          <Node ref={reports}>
            <ChartIcon className="size-5" />
          </Node>
          <Node ref={inbox}>
            <InboxIcon className="size-5" />
          </Node>
        </div>
        <AnimatedBeam containerRef={container} fromRef={orders} toRef={hub} curvature={-40} />
        <AnimatedBeam containerRef={container} fromRef={customers} toRef={hub} duration={5} />
        <AnimatedBeam containerRef={container} fromRef={invoices} toRef={hub} curvature={40} duration={6} />
        <AnimatedBeam containerRef={container} fromRef={hub} toRef={reports} curvature={-30} duration={7} reverse />
        <AnimatedBeam containerRef={container} fromRef={hub} toRef={inbox} curvature={30} duration={6.5} reverse />
      </div>
    </div>
  );
}

Installation

npx rdloom add animated-beam

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/animated-beam.json

Works, but without upgrade tracking, and you import the motion CSS yourself.

Usage

import { AnimatedBeam, ChartIcon, CreditCardIcon, InboxIcon, SparkleIcon, UsersIcon, FileIcon } from "@rdloom/react";

    <div
      ref={ref}
      className={`relative z-10 flex items-center justify-center rounded-full border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] text-[var(--rd-color-text-default)] [box-shadow:var(--rd-elevation-raised)] ${
        big ? "size-16" : "size-12"
      }`}
    >
      {children}
    </div>
  );
});

// Several sources feed one hub, and the hub feeds two places. Each beam is a decorative line between two real elements.
export default function AnimatedBeamBasicExample() {
  const container = useRef<HTMLDivElement>(null);
  const orders = useRef<HTMLDivElement>(null);
  const customers = useRef<HTMLDivElement>(null);
  const invoices = useRef<HTMLDivElement>(null);
  const hub = useRef<HTMLDivElement>(null);
  const reports = useRef<HTMLDivElement>(null);
  const inbox = useRef<HTMLDivElement>(null);
  return (
    <div className="flex w-full justify-center">
      <div ref={container} className="relative flex h-80 w-full max-w-2xl items-center justify-between px-6">
        <div className="flex flex-col gap-8">
          <Node ref={orders}>
            <FileIcon className="size-5" />
          </Node>
          <Node ref={customers}>
            <UsersIcon className="size-5" />
          </Node>
          <Node ref={invoices}>
            <CreditCardIcon className="size-5" />
          </Node>
        </div>
        <Node ref={hub} big>
          <SparkleIcon className="size-7" />
        </Node>
        <div className="flex flex-col gap-16">
          <Node ref={reports}>
            <ChartIcon className="size-5" />
          </Node>
          <Node ref={inbox}>
            <InboxIcon className="size-5" />
          </Node>
        </div>
        <AnimatedBeam containerRef={container} fromRef={orders} toRef={hub} curvature={-40} />
        <AnimatedBeam containerRef={container} fromRef={customers} toRef={hub} duration={5} />
        <AnimatedBeam containerRef={container} fromRef={invoices} toRef={hub} curvature={40} duration={6} />
        <AnimatedBeam containerRef={container} fromRef={hub} toRef={reports} curvature={-30} duration={7} reverse />
        <AnimatedBeam containerRef={container} fromRef={hub} toRef={inbox} curvature={30} duration={6.5} reverse />
      </div>
    </div>

Curved

Form
Queue
Report
import { useRef } from "react";
import { AnimatedBeam } from "@rdloom/react";

export default function AnimatedBeamCurvedExample() {
  const container = useRef<HTMLDivElement>(null);
  const a = useRef<HTMLDivElement>(null);
  const b = useRef<HTMLDivElement>(null);
  const c = useRef<HTMLDivElement>(null);
  const box = "relative z-10 rounded-[var(--rd-radius-control)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] px-4 py-3 text-sm";
  return (
    <div className="flex w-full justify-center">
      <div ref={container} className="relative flex w-full max-w-2xl items-center justify-between px-6 py-14">
        <div ref={a} className={box}>Form</div>
        <div ref={b} className={box}>Queue</div>
        <div ref={c} className={box}>Report</div>
        <AnimatedBeam containerRef={container} fromRef={a} toRef={b} curvature={-40} />
        <AnimatedBeam containerRef={container} fromRef={b} toRef={c} curvature={40} reverse duration={4} />
      </div>
    </div>
  );
}

With ids

Source
Target
import { useRef } from "react";
import { AnimatedBeam } from "@rdloom/react";

export default function AnimatedBeamWithIdsExample() {
  const container = useRef<HTMLDivElement>(null);
  const box = "relative z-10 rounded-[var(--rd-radius-control)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] px-4 py-3 text-sm";
  return (
    <div className="flex w-full justify-center">
      <div ref={container} className="relative flex w-full max-w-2xl items-center justify-between px-6 py-10">
        <div id="beam-source" className={box}>Source</div>
        <div id="beam-target" className={box}>Target</div>
        <AnimatedBeam containerRef={container} fromId="beam-source" toId="beam-target" pathWidth={3} />
      </div>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
containerRefrequired

Ref of the positioned element that holds both endpoints.

RefObject<HTMLElement | null>none
fromRef

Ref of the element the line starts at. Use this or fromId.

RefObject<HTMLElement | null>none
toRef

Ref of the element the line ends at. Use this or toId.

RefObject<HTMLElement | null>none
fromId

The id of the start element, when a ref is not handy.

stringnone
toId

The id of the end element, when a ref is not handy.

stringnone
curvature

How far the line bows away from a straight one, in pixels. Negative bows the other way.

number0
reverse

The light travels from the end to the start.

booleanfalse
duration

Seconds for the light to cross.

number3
pathColor

Colour of the resting line, any CSS colour. Defaults to the border colour.

stringnone
beamColor

Colour of the travelling light, any CSS colour. Defaults to the primary action colour.

stringnone
pathWidth

Line width in pixels.

number2
isPaused

Stops the effect and shows it still.

booleanfalse

Accessibility

Role presentation, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

Not interactive.

Screen readers announce

  • Not announced: the canvas is hidden from assistive technology

What your code must do

  • Decoration only: the effect is aria-hidden, never takes focus and never blocks the pointer
  • Stands still for visitors who ask for less motion (prefers-reduced-motion) and inside an rdm-still container, and isPaused stops it too
  • Does not flash: nothing changes more than three times in a second (WCAG 2.3.1)
  • The resting line stays visible when the light is still, so the connection is never lost
  • The connection must also be stated in text near the diagram: the line alone says nothing to a screen reader
  • In forced-colors (high contrast) mode the canvas is hidden
  • Redraws when the container or an endpoint changes size, and is paused while off-screen

Guidelines

Use it when

  • A flow between two or three boxes in a diagram
  • Showing that two services are connected

Avoid it when

  • Carrying meaning that is not also written down
  • Many lines that all move at once

Don't

  • Relying on the line to explain the relationship
  • Leaving the container without position: relative, so the canvas escapes it

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-border-default
  • --rd-color-action-primary