UI Components Medium
Animated Beam
SVG animated beam/line connecting two elements with a flowing dash animation, perfect for flow diagrams and connection visualizations.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
}
/* --- Beam Container --- */
.beam-container {
position: relative;
width: min(700px, calc(100vw - 2rem));
height: 300px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem 3rem;
}
.beam-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: visible;
}
/* --- Nodes --- */
.beam-node {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.beam-node__icon {
width: 56px;
height: 56px;
display: grid;
place-items: center;
font-size: 1.25rem;
border-radius: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e2e8f0;
box-shadow: 0 0 20px rgba(34, 211, 238, 0.15);
transition: box-shadow 0.3s ease;
}
.beam-node--from .beam-node__icon {
color: #22d3ee;
border-color: rgba(34, 211, 238, 0.3);
}
.beam-node--middle .beam-node__icon {
color: #a855f7;
border-color: rgba(168, 85, 247, 0.3);
box-shadow: 0 0 20px rgba(168, 85, 247, 0.15);
}
.beam-node--to .beam-node__icon {
color: #34d399;
border-color: rgba(52, 211, 153, 0.3);
box-shadow: 0 0 20px rgba(52, 211, 153, 0.15);
}
.beam-node__label {
font-size: 0.8125rem;
font-weight: 500;
color: #94a3b8;
letter-spacing: 0.02em;
}
/* --- Animated Path --- */
.beam-path {
fill: none;
stroke-width: 2;
stroke-linecap: round;
}
.beam-path--bg {
stroke: rgba(255, 255, 255, 0.06);
stroke-width: 2;
}
.beam-path--animated {
stroke: url(#beamGradient);
stroke-dasharray: 16 24;
stroke-dashoffset: 0;
animation: beam-dash 2s linear infinite;
}
.beam-path--animated-2 {
stroke: url(#beamGradient2);
stroke-dasharray: 16 24;
stroke-dashoffset: 0;
animation: beam-dash 2.5s linear infinite;
}
@keyframes beam-dash {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -40;
}
}
/* Glow filter */
.beam-glow {
filter: blur(4px);
opacity: 0.5;
}// Animated Beam — draws SVG paths between elements with flowing dash animation.
(function () {
"use strict";
const container = document.getElementById("beamContainer");
const svg = document.getElementById("beamSvg");
const nodeFrom = document.getElementById("nodeFrom");
const nodeMiddle = document.getElementById("nodeMiddle");
const nodeTo = document.getElementById("nodeTo");
if (!container || !svg || !nodeFrom || !nodeMiddle || !nodeTo) return;
function getCenter(el) {
const containerRect = container.getBoundingClientRect();
const rect = el.querySelector(".beam-node__icon").getBoundingClientRect();
return {
x: rect.left + rect.width / 2 - containerRect.left,
y: rect.top + rect.height / 2 - containerRect.top,
};
}
function createCurvePath(from, to, curvature) {
const midX = (from.x + to.x) / 2;
const cp1x = midX;
const cp1y = from.y + curvature;
const cp2x = midX;
const cp2y = to.y + curvature;
return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;
}
function drawBeams() {
const fromPos = getCenter(nodeFrom);
const midPos = getCenter(nodeMiddle);
const toPos = getCenter(nodeTo);
const path1 = createCurvePath(fromPos, midPos, -40);
const path2 = createCurvePath(midPos, toPos, 40);
svg.innerHTML = `
<defs>
<linearGradient id="beamGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.1" />
<stop offset="50%" stop-color="#22d3ee" stop-opacity="1" />
<stop offset="100%" stop-color="#a855f7" stop-opacity="0.1" />
</linearGradient>
<linearGradient id="beamGradient2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#a855f7" stop-opacity="0.1" />
<stop offset="50%" stop-color="#a855f7" stop-opacity="1" />
<stop offset="100%" stop-color="#34d399" stop-opacity="0.1" />
</linearGradient>
</defs>
<!-- Background paths -->
<path class="beam-path beam-path--bg" d="${path1}" />
<path class="beam-path beam-path--bg" d="${path2}" />
<!-- Glow layers -->
<path class="beam-path beam-path--animated beam-glow" d="${path1}" />
<path class="beam-path beam-path--animated-2 beam-glow" d="${path2}" />
<!-- Main animated beams -->
<path class="beam-path beam-path--animated" d="${path1}" />
<path class="beam-path beam-path--animated-2" d="${path2}" />
`;
}
drawBeams();
window.addEventListener("resize", drawBeams);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Animated Beam</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="beam-container" id="beamContainer">
<svg class="beam-svg" id="beamSvg"></svg>
<div class="beam-node beam-node--from" id="nodeFrom">
<span class="beam-node__icon">◆</span>
<span class="beam-node__label">Source</span>
</div>
<div class="beam-node beam-node--middle" id="nodeMiddle">
<span class="beam-node__icon">★</span>
<span class="beam-node__label">Process</span>
</div>
<div class="beam-node beam-node--to" id="nodeTo">
<span class="beam-node__icon">●</span>
<span class="beam-node__label">Output</span>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import {
useRef,
useEffect,
useState,
useCallback,
type RefObject,
type ReactNode,
type CSSProperties,
} from "react";
interface Point {
x: number;
y: number;
}
interface AnimatedBeamProps {
containerRef: RefObject<HTMLDivElement | null>;
fromRef: RefObject<HTMLDivElement | null>;
toRef: RefObject<HTMLDivElement | null>;
curvature?: number;
gradientStartColor?: string;
gradientEndColor?: string;
dashSpeed?: string;
}
function AnimatedBeam({
containerRef,
fromRef,
toRef,
curvature = -40,
gradientStartColor = "#22d3ee",
gradientEndColor = "#a855f7",
dashSpeed = "2s",
}: AnimatedBeamProps) {
const [pathD, setPathD] = useState("");
const gradientId = useRef(`beam-grad-${Math.random().toString(36).slice(2, 9)}`);
const updatePath = useCallback(() => {
const container = containerRef.current;
const from = fromRef.current;
const to = toRef.current;
if (!container || !from || !to) return;
const cr = container.getBoundingClientRect();
const fr = from.getBoundingClientRect();
const tr = to.getBoundingClientRect();
const start: Point = {
x: fr.left + fr.width / 2 - cr.left,
y: fr.top + fr.height / 2 - cr.top,
};
const end: Point = {
x: tr.left + tr.width / 2 - cr.left,
y: tr.top + tr.height / 2 - cr.top,
};
const midX = (start.x + end.x) / 2;
setPathD(
`M ${start.x} ${start.y} C ${midX} ${start.y + curvature}, ${midX} ${end.y + curvature}, ${end.x} ${end.y}`
);
}, [containerRef, fromRef, toRef, curvature]);
useEffect(() => {
updatePath();
window.addEventListener("resize", updatePath);
return () => window.removeEventListener("resize", updatePath);
}, [updatePath]);
const dashAnim: CSSProperties = {
fill: "none",
stroke: `url(#${gradientId.current})`,
strokeWidth: 2,
strokeLinecap: "round" as const,
strokeDasharray: "16 24",
animation: `animated-beam-dash ${dashSpeed} linear infinite`,
};
return (
<svg
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
overflow: "visible",
}}
>
<defs>
<linearGradient id={gradientId.current} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={gradientStartColor} stopOpacity={0.1} />
<stop offset="50%" stopColor={gradientStartColor} stopOpacity={1} />
<stop offset="100%" stopColor={gradientEndColor} stopOpacity={0.1} />
</linearGradient>
</defs>
{/* Background line */}
<path
d={pathD}
fill="none"
stroke="rgba(255,255,255,0.06)"
strokeWidth={2}
strokeLinecap="round"
/>
{/* Glow */}
<path d={pathD} style={{ ...dashAnim, filter: "blur(4px)", opacity: 0.5 }} />
{/* Main beam */}
<path d={pathD} style={dashAnim} />
</svg>
);
}
interface NodeProps {
children: ReactNode;
style?: CSSProperties;
}
function BeamNode({ children, style }: NodeProps) {
return (
<div
style={{
position: "relative",
zIndex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.75rem",
...style,
}}
>
{children}
</div>
);
}
const iconBoxStyle = (color: string): CSSProperties => ({
width: 56,
height: 56,
display: "grid",
placeItems: "center",
fontSize: "1.25rem",
borderRadius: "1rem",
background: "rgba(255,255,255,0.05)",
border: `1px solid ${color}44`,
color,
boxShadow: `0 0 20px ${color}26`,
});
// Demo usage
export default function AnimatedBeamDemo() {
const containerRef = useRef<HTMLDivElement>(null);
const fromRef = useRef<HTMLDivElement>(null);
const midRef = useRef<HTMLDivElement>(null);
const toRef = useRef<HTMLDivElement>(null);
return (
<div
style={{
minHeight: "100vh",
display: "grid",
placeItems: "center",
background: "#0a0a0a",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<style>{`
@keyframes animated-beam-dash {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -40; }
}
`}</style>
<div
ref={containerRef}
style={{
position: "relative",
width: "min(700px, calc(100vw - 2rem))",
height: 300,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "2rem 3rem",
}}
>
<AnimatedBeam
containerRef={containerRef}
fromRef={fromRef}
toRef={midRef}
curvature={-40}
gradientStartColor="#22d3ee"
gradientEndColor="#a855f7"
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={midRef}
toRef={toRef}
curvature={40}
gradientStartColor="#a855f7"
gradientEndColor="#34d399"
dashSpeed="2.5s"
/>
<BeamNode>
<div ref={fromRef} style={iconBoxStyle("#22d3ee")}>
◆
</div>
<span style={{ fontSize: "0.8125rem", fontWeight: 500, color: "#94a3b8" }}>Source</span>
</BeamNode>
<BeamNode>
<div ref={midRef} style={iconBoxStyle("#a855f7")}>
★
</div>
<span style={{ fontSize: "0.8125rem", fontWeight: 500, color: "#94a3b8" }}>Process</span>
</BeamNode>
<BeamNode>
<div ref={toRef} style={iconBoxStyle("#34d399")}>
●
</div>
<span style={{ fontSize: "0.8125rem", fontWeight: 500, color: "#94a3b8" }}>Output</span>
</BeamNode>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const containerEl = ref(null);
const fromEl = ref(null);
const midEl = ref(null);
const toEl = ref(null);
const pathD1 = ref("");
const pathD2 = ref("");
const gradientId1 = "beam-grad-" + Math.random().toString(36).slice(2, 9);
const gradientId2 = "beam-grad-" + Math.random().toString(36).slice(2, 9);
function updatePaths() {
if (!containerEl.value || !fromEl.value || !midEl.value || !toEl.value) return;
const cr = containerEl.value.getBoundingClientRect();
const fr = fromEl.value.getBoundingClientRect();
const mr = midEl.value.getBoundingClientRect();
const tr = toEl.value.getBoundingClientRect();
const fromCenter = {
x: fr.left + fr.width / 2 - cr.left,
y: fr.top + fr.height / 2 - cr.top,
};
const midCenter = {
x: mr.left + mr.width / 2 - cr.left,
y: mr.top + mr.height / 2 - cr.top,
};
const toCenter = {
x: tr.left + tr.width / 2 - cr.left,
y: tr.top + tr.height / 2 - cr.top,
};
const midX1 = (fromCenter.x + midCenter.x) / 2;
pathD1.value = `M ${fromCenter.x} ${fromCenter.y} C ${midX1} ${fromCenter.y + -40}, ${midX1} ${midCenter.y + -40}, ${midCenter.x} ${midCenter.y}`;
const midX2 = (midCenter.x + toCenter.x) / 2;
pathD2.value = `M ${midCenter.x} ${midCenter.y} C ${midX2} ${midCenter.y + 40}, ${midX2} ${toCenter.y + 40}, ${toCenter.x} ${toCenter.y}`;
}
function iconBoxStyle(color) {
return {
width: "56px",
height: "56px",
display: "grid",
placeItems: "center",
fontSize: "1.25rem",
borderRadius: "1rem",
background: "rgba(255,255,255,0.05)",
border: `1px solid ${color}44`,
color: color,
boxShadow: `0 0 20px ${color}26`,
};
}
onMounted(() => {
updatePaths();
window.addEventListener("resize", updatePaths);
});
onUnmounted(() => {
window.removeEventListener("resize", updatePaths);
});
</script>
<template>
<div
style="min-height: 100vh; display: grid; place-items: center; background: #0a0a0a; font-family: system-ui, -apple-system, sans-serif;"
>
<div
ref="containerEl"
style="position: relative; width: min(700px, calc(100vw - 2rem)); height: 300px; display: flex; align-items: center; justify-content: space-between; padding: 2rem 3rem;"
>
<svg style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible;">
<defs>
<linearGradient :id="gradientId1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.1" />
<stop offset="50%" stop-color="#22d3ee" stop-opacity="1" />
<stop offset="100%" stop-color="#a855f7" stop-opacity="0.1" />
</linearGradient>
<linearGradient :id="gradientId2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#a855f7" stop-opacity="0.1" />
<stop offset="50%" stop-color="#a855f7" stop-opacity="1" />
<stop offset="100%" stop-color="#34d399" stop-opacity="0.1" />
</linearGradient>
</defs>
<!-- Beam 1 -->
<path :d="pathD1" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="2" stroke-linecap="round" />
<path :d="pathD1" class="beam-dash" :style="{ stroke: `url(#${gradientId1})`, filter: 'blur(4px)', opacity: 0.5 }" />
<path :d="pathD1" class="beam-dash" :style="{ stroke: `url(#${gradientId1})` }" />
<!-- Beam 2 -->
<path :d="pathD2" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="2" stroke-linecap="round" />
<path :d="pathD2" class="beam-dash beam-dash-slow" :style="{ stroke: `url(#${gradientId2})`, filter: 'blur(4px)', opacity: 0.5 }" />
<path :d="pathD2" class="beam-dash beam-dash-slow" :style="{ stroke: `url(#${gradientId2})` }" />
</svg>
<div style="position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div ref="fromEl" :style="iconBoxStyle('#22d3ee')">◆</div>
<span style="font-size: 0.8125rem; font-weight: 500; color: #94a3b8;">Source</span>
</div>
<div style="position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div ref="midEl" :style="iconBoxStyle('#a855f7')">★</div>
<span style="font-size: 0.8125rem; font-weight: 500; color: #94a3b8;">Process</span>
</div>
<div style="position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div ref="toEl" :style="iconBoxStyle('#34d399')">●</div>
<span style="font-size: 0.8125rem; font-weight: 500; color: #94a3b8;">Output</span>
</div>
</div>
</div>
</template>
<style scoped>
@keyframes animated-beam-dash {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -40; }
}
.beam-dash {
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-dasharray: 16 24;
animation: animated-beam-dash 2s linear infinite;
}
.beam-dash-slow {
animation-duration: 2.5s;
}
</style><script>
import { onMount, onDestroy } from "svelte";
let containerEl;
let fromEl;
let midEl;
let toEl;
let pathD1 = "";
let pathD2 = "";
let gradientId1 = "beam-grad-" + Math.random().toString(36).slice(2, 9);
let gradientId2 = "beam-grad-" + Math.random().toString(36).slice(2, 9);
function updatePaths() {
if (!containerEl || !fromEl || !midEl || !toEl) return;
const cr = containerEl.getBoundingClientRect();
const fr = fromEl.getBoundingClientRect();
const mr = midEl.getBoundingClientRect();
const tr = toEl.getBoundingClientRect();
const fromCenter = {
x: fr.left + fr.width / 2 - cr.left,
y: fr.top + fr.height / 2 - cr.top,
};
const midCenter = {
x: mr.left + mr.width / 2 - cr.left,
y: mr.top + mr.height / 2 - cr.top,
};
const toCenter = {
x: tr.left + tr.width / 2 - cr.left,
y: tr.top + tr.height / 2 - cr.top,
};
const midX1 = (fromCenter.x + midCenter.x) / 2;
pathD1 = `M ${fromCenter.x} ${fromCenter.y} C ${midX1} ${fromCenter.y + -40}, ${midX1} ${midCenter.y + -40}, ${midCenter.x} ${midCenter.y}`;
const midX2 = (midCenter.x + toCenter.x) / 2;
pathD2 = `M ${midCenter.x} ${midCenter.y} C ${midX2} ${midCenter.y + 40}, ${midX2} ${toCenter.y + 40}, ${toCenter.x} ${toCenter.y}`;
}
onMount(() => {
updatePaths();
window.addEventListener("resize", updatePaths);
});
onDestroy(() => {
window.removeEventListener("resize", updatePaths);
});
function iconBoxStyle(color) {
return `width: 56px; height: 56px; display: grid; place-items: center; font-size: 1.25rem; border-radius: 1rem; background: rgba(255,255,255,0.05); border: 1px solid ${color}44; color: ${color}; box-shadow: 0 0 20px ${color}26;`;
}
</script>
<div
style="min-height: 100vh; display: grid; place-items: center; background: #0a0a0a; font-family: system-ui, -apple-system, sans-serif;"
>
<div
bind:this={containerEl}
style="position: relative; width: min(700px, calc(100vw - 2rem)); height: 300px; display: flex; align-items: center; justify-content: space-between; padding: 2rem 3rem;"
>
<svg style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible;">
<defs>
<linearGradient id={gradientId1} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.1" />
<stop offset="50%" stop-color="#22d3ee" stop-opacity="1" />
<stop offset="100%" stop-color="#a855f7" stop-opacity="0.1" />
</linearGradient>
<linearGradient id={gradientId2} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#a855f7" stop-opacity="0.1" />
<stop offset="50%" stop-color="#a855f7" stop-opacity="1" />
<stop offset="100%" stop-color="#34d399" stop-opacity="0.1" />
</linearGradient>
</defs>
<!-- Beam 1 -->
<path d={pathD1} fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="2" stroke-linecap="round" />
<path d={pathD1} class="beam-dash" style="stroke: url(#{gradientId1}); filter: blur(4px); opacity: 0.5;" />
<path d={pathD1} class="beam-dash" style="stroke: url(#{gradientId1});" />
<!-- Beam 2 -->
<path d={pathD2} fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="2" stroke-linecap="round" />
<path d={pathD2} class="beam-dash beam-dash-slow" style="stroke: url(#{gradientId2}); filter: blur(4px); opacity: 0.5;" />
<path d={pathD2} class="beam-dash beam-dash-slow" style="stroke: url(#{gradientId2});" />
</svg>
<div style="position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div bind:this={fromEl} style={iconBoxStyle('#22d3ee')}>◆</div>
<span style="font-size: 0.8125rem; font-weight: 500; color: #94a3b8;">Source</span>
</div>
<div style="position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div bind:this={midEl} style={iconBoxStyle('#a855f7')}>★</div>
<span style="font-size: 0.8125rem; font-weight: 500; color: #94a3b8;">Process</span>
</div>
<div style="position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div bind:this={toEl} style={iconBoxStyle('#34d399')}>●</div>
<span style="font-size: 0.8125rem; font-weight: 500; color: #94a3b8;">Output</span>
</div>
</div>
</div>
<style>
@keyframes animated-beam-dash {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: -40; }
}
.beam-dash {
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-dasharray: 16 24;
animation: animated-beam-dash 2s linear infinite;
}
.beam-dash-slow {
animation-duration: 2.5s;
}
</style>Animated Beam
An SVG animated beam/line that connects two DOM elements with a flowing dash animation. Ideal for flow diagrams, connection visualizations, and interactive node-based UIs.
How it works
- JavaScript calculates the center positions of two elements
- An SVG path is drawn between them with a cubic bezier curve
stroke-dasharrayandstroke-dashoffsetare animated via CSS keyframes- A gradient along the path creates the “beam” illumination effect
Customization
- Change beam color via
--beam-color - Adjust curvature with the
curvatureparameter - Control animation speed with
--beam-dash-speed
When to use it
- Flow diagrams and architecture visualizations
- Connecting UI cards or nodes
- Animated decorative connections between sections