A beam of light traveling the border, shifting color as it moves. Pure CSS engine — a rotating conic mask over stationary gradients, with a blurred bloom halo riding the beam head.
npx shadcn@latest add @paragon/border-beam"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface BorderBeamProps extends React.ComponentProps<"div"> {
/** Seconds per revolution. */
duration?: number;
/** Border ring thickness in px. */
borderWidth?: number;
/** Overall effect opacity, 0–1. */
strength?: number;
/** Soft halo riding the beam head, 0–1. 0 removes the bloom layer. */
bloom?: number;
/** Corner radius of the ring in px. Match the parent's radius. */
borderRadius?: number;
/** Beam colors, painted as stationary glows the beam travels across. */
colors?: string[];
}
const clamp01 = (value: number) => Math.min(Math.max(value, 0), 1);
/**
* A beam of light traveling the border of the parent element.
*
* Technique: stationary multi-hue radial gradients are parked around the
* perimeter; only a conic-gradient mask window (a registered @property angle,
* animated once on the wrapper and inherited by every layer) rotates over
* them, so the beam shifts color as it travels for the cost of one
* interpolated value. Two layers ride that angle: a crisp stroke confined to
* the border via the double-mask ring trick, and a bloom — the same masked
* ring under a parent blur, isolated on its own element so the halo bleeds
* softly past the border without ever touching the stroke. The spin pauses
* offscreen via IntersectionObserver; reduced motion parks the beam as a
* static gradient arc. Absolutely positioned — parent needs
* position: relative.
*/
export function BorderBeam({
duration = 6,
borderWidth = 1,
strength = 1,
bloom = 0.5,
borderRadius = 12,
colors = ["#f43f5e", "#3b82f6", "#22c55e", "#a855f7", "#f97316"],
className,
style,
ref: forwardedRef,
...props
}: BorderBeamProps) {
const id = React.useId().replace(/[^a-zA-Z0-9-]/g, "");
const localRef = React.useRef<HTMLDivElement | null>(null);
const [inView, setInView] = React.useState(true);
React.useEffect(() => {
const node = localRef.current;
if (!node) return;
const observer = new IntersectionObserver(([entry]) => {
setInView(entry?.isIntersecting ?? true);
});
observer.observe(node);
return () => observer.disconnect();
}, []);
const positions = [
"20% 0%",
"100% 30%",
"80% 100%",
"0% 70%",
"55% 100%",
];
const field = colors
.map(
(color, i) =>
`radial-gradient(ellipse 120px 60px at ${positions[i % positions.length]}, ${color}, transparent)`,
)
.join(",");
/** One ring layer: the color field, masked to the rotating beam window
* (multi-stop tail → head) intersected with the border ring. */
const ringLayer = (padding: number): React.CSSProperties => ({
position: "absolute",
inset: 0,
borderRadius,
padding,
background: field,
mask: `conic-gradient(from var(--beam-angle-${id}), transparent 0%, transparent 55%, rgba(255,255,255,0.4) 70%, white 80%, white 92%, transparent 98%), linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMaskComposite: "source-in, xor",
maskComposite: "intersect, exclude",
});
const level = clamp01(strength);
const bloomLevel = clamp01(bloom);
return (
<>
<style href={`paragon-border-beam-${id}`} precedence="paragon">{`
@property --beam-angle-${id} {
syntax: "<angle>";
initial-value: 0deg;
inherits: true;
}
@keyframes beam-spin-${id} {
0% { --beam-angle-${id}: 0deg; }
25% { --beam-angle-${id}: 90deg; }
50% { --beam-angle-${id}: 180deg; }
75% { --beam-angle-${id}: 270deg; }
100% { --beam-angle-${id}: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
[data-beam="${id}"] { animation: none !important; }
}
`}</style>
<div
aria-hidden
data-beam={id}
ref={(node) => {
localRef.current = node;
if (typeof forwardedRef === "function") forwardedRef(node);
else if (forwardedRef) forwardedRef.current = node;
}}
className={cn("pointer-events-none absolute inset-0", className)}
style={{
borderRadius,
opacity: 0.9 * level,
animation: `beam-spin-${id} ${duration}s linear infinite`,
animationPlayState: inView ? "running" : "paused",
...style,
}}
{...props}
>
{bloomLevel > 0 && (
// Blur isolated on its own element: the masked ring below is
// re-rasterized through this parent filter, so the halo spreads
// both ways across the border instead of being clipped by the mask.
<div
style={{
position: "absolute",
inset: 0,
filter: "blur(6px)",
opacity: 0.65 * bloomLevel,
}}
>
<div style={ringLayer(borderWidth + 1)} />
</div>
)}
<div style={ringLayer(borderWidth)} />
</div>
</>
);
}