Animation & motionAnimated beam connections
Threads coming together
Moving highlights trace three measured paths into a shared destination.

Play or pause the connections
Add to your project
Component source
Standalone React component. Styles are included.
React only. No additional packages.
Render the default export inside a container with a defined height. Its optional active prop stops movement when false. This example works with React 19; Next.js projects can use it as a client component.
View source code
/**
* Adapted from https://github.com/magicuidesign/magicui/blob/85f97fd27b053339af74ba9dd4ca428ee25989fb/apps/www/registry/magicui/animated-beam.tsx
* Retains measured element-center quadratic paths, ResizeObserver updates and a traveling four-stop SVG gradient. Native SVG animation replaces Motion; adds explicit pause, bounded geometry and reduced-motion still frame.
*
* MIT License
*
* Copyright (c) Magic UI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use client"
import { useEffect, useId, useRef, useState } from "react"
export default function AnimatedBeam({ active = true }: { active?: boolean }) {
const [paused, setPaused] = useState(false)
const [geometry, setGeometry] = useState({ width: 300, height: 174, paths: [] as string[] })
const container = useRef<HTMLDivElement>(null)
const target = useRef<HTMLDivElement>(null)
const sources = useRef<Array<HTMLDivElement | null>>([])
const svg = useRef<SVGSVGElement>(null)
const id = useId().replace(/:/g, "")
useEffect(() => {
const element = container.current
if (!element) return
function updatePath() {
const box = element!.getBoundingClientRect()
const end = target.current?.getBoundingClientRect()
if (!end || !box.width || !box.height) return
const width = element!.offsetWidth,
height = element!.offsetHeight,
scaleX = width / box.width,
scaleY = height / box.height
const endX = (end.left - box.left + end.width / 2) * scaleX
const endY = (end.top - box.top + end.height / 2) * scaleY
const paths = sources.current.map((source) => {
const rect = source!.getBoundingClientRect()
const startX = (rect.left - box.left + rect.width / 2) * scaleX
const startY = (rect.top - box.top + rect.height / 2) * scaleY
return `M ${startX},${startY} Q ${(startX + endX) / 2},${startY} ${endX},${endY}`
})
setGeometry({ width, height, paths })
}
const observer = new ResizeObserver(updatePath)
observer.observe(element)
updatePath()
return () => observer.disconnect()
}, [])
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
function sync() {
const element = svg.current
if (!element) return
if (!active || paused || media.matches) {
element.pauseAnimations()
if (media.matches) element.setCurrentTime(1.4)
} else element.unpauseAnimations()
}
sync()
media.addEventListener("change", sync)
return () => media.removeEventListener("change", sync)
}, [active, paused])
return (
<div className="mg-beam">
<style>{`
.mg-beam{width:100%;height:100%;min-height:260px;display:grid;place-content:center;justify-items:center;position:relative;background:#edf0e5;color:#465342;font-family:inherit}
.mg-beam .mg-beam-scene{position:relative;width:min(300px,calc(100vw - 72px));height:174px;display:flex;justify-content:space-between;align-items:center;box-sizing:border-box;padding:0 20px}
.mg-beam .mg-beam-sources{display:flex;flex-direction:column;gap:16px}.mg-beam .mg-beam-node{position:relative;z-index:1;width:50px;height:40px;display:grid;place-items:center;border:1px solid #c6ceb8;border-radius:11px;background:#f9faf3;color:#697458;font-size:10px;box-shadow:0 4px 8px #40513408}.mg-beam .mg-beam-target{width:69px;height:69px;border-radius:20px;background:#516148;border-color:#677757;color:#f1f6e8;font-size:12px}
.mg-beam .mg-beam-lines{position:absolute;inset:0;pointer-events:none;overflow:visible}
.mg-beam .mg-beam-pause{position:absolute;right:12px;bottom:0;min-height:44px;padding:0 8px;font:inherit;font-size:10px;border:0;background:none;color:#5f6a57;cursor:pointer}
@media(prefers-reduced-motion:reduce){.mg-beam .mg-beam-pause{display:none}}
`}</style>
<div
className="mg-beam-scene"
ref={container}
role="img"
aria-label="Words, shapes and ideas connect into one story"
>
<svg
ref={svg}
className="mg-beam-lines"
aria-hidden="true"
width={geometry.width}
height={geometry.height}
viewBox={`0 0 ${geometry.width} ${geometry.height}`}
fill="none"
>
<defs>
{geometry.paths.map((_, i) => (
<linearGradient key={i} id={`${id}-${i}`} gradientUnits="userSpaceOnUse" x1="10%" x2="0%" y1="0%" y2="0%">
<animate
attributeName="x1"
values="10%;110%"
dur="4s"
begin={`${i * 0.7}s`}
repeatCount="indefinite"
calcMode="spline"
keyTimes="0;1"
keySplines="0.16 1 0.3 1"
/>
<animate
attributeName="x2"
values="0%;100%"
dur="4s"
begin={`${i * 0.7}s`}
repeatCount="indefinite"
calcMode="spline"
keyTimes="0;1"
keySplines="0.16 1 0.3 1"
/>
<stop stopColor="#d6be80" stopOpacity="0" />
<stop stopColor="#d6be80" />
<stop offset="32.5%" stopColor="#849f6a" />
<stop offset="100%" stopColor="#849f6a" stopOpacity="0" />
</linearGradient>
))}
</defs>
{geometry.paths.map((path, i) => (
<g key={i}>
<path d={path} stroke="#c4cebb" strokeWidth="1" />
<path d={path} stroke={`url(#${id}-${i})`} strokeWidth="2" strokeLinecap="round" />
</g>
))}
</svg>
<div className="mg-beam-sources">
{["Words", "Shapes", "Ideas"].map((label, i) => (
<div
key={label}
className="mg-beam-node"
ref={(node) => {
sources.current[i] = node
}}
aria-hidden="true"
>
{label}
</div>
))}
</div>
<div className="mg-beam-node mg-beam-target" ref={target} aria-hidden="true">
A story
</div>
</div>
<button className="mg-beam-pause" type="button" aria-pressed={paused} onClick={() => setPaused(!paused)}>
{paused ? "Play connections" : "Pause connections"}
</button>
</div>
)
}
Where it works well
Explain aggregation, integrations or a simple flow of information.
Reduced motion
The SVG timeline freezes at a representative point; the full connection diagram remains visible.
Things to know
Uses native SVG animation and ResizeObserver. This illustrates connections; it does not track actual task completion.