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.
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-beamCopies 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.jsonWorks, 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
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
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.
| Prop | Type | Default |
|---|---|---|
containerRefrequiredRef of the positioned element that holds both endpoints. | RefObject<HTMLElement | null> | none |
fromRefRef of the element the line starts at. Use this or fromId. | RefObject<HTMLElement | null> | none |
toRefRef of the element the line ends at. Use this or toId. | RefObject<HTMLElement | null> | none |
fromIdThe id of the start element, when a ref is not handy. | string | none |
toIdThe id of the end element, when a ref is not handy. | string | none |
curvatureHow far the line bows away from a straight one, in pixels. Negative bows the other way. | number | 0 |
reverseThe light travels from the end to the start. | boolean | false |
durationSeconds for the light to cross. | number | 3 |
pathColorColour of the resting line, any CSS colour. Defaults to the border colour. | string | none |
beamColorColour of the travelling light, any CSS colour. Defaults to the primary action colour. | string | none |
pathWidthLine width in pixels. | number | 2 |
isPausedStops the effect and shows it still. | boolean | false |
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