backgroundBeams
A Backgrounds block/component from Aceternity UI — clean-room reimplemented for Domphy (see methodology). Call backgroundBeams() with no arguments for a working demo, or edit the code below live.
Implementation notes
Uses the package's existing 'static path, moving gradient' technique (same idiom as animatedBeam.ts): procedurally generated S-curve fibers with a per-beam linearGradient sliding through objectBoundingBox coordinates, driven by a single IntersectionObserver-gated rAF loop. Two intentional divergences from the reference: (1) default beam count capped at 20 rather than ~50 for perf, per the spec's own research note; (2) the reference's literal cyan/purple/magenta or orange/red hex gradient stops are replaced with cycling Domphy ThemeColor roles (default info/primary/secondary) since raw hex/rgb is forbidden by doctor rules — visually reads as multi-hued across the beam field but each single beam's band is limited to the theme's own color ramp rather than an arbitrary hue.
Status: partial · Reference: Aceternity UI original
// Aceternity UI "Background Beams" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied). An
// ambient hero-section background made of many long, gently curved SVG
// fibers overlapping on a dark backdrop, each carrying its own softly
// traveling color band.
//
// The stroke technique is the same "static path, moving gradient" idiom this
// package already uses for `animatedBeam()`: the `<path>` shapes themselves
// never move — only each path's own `<linearGradient>` slides its stops along
// a diagonal window, which reads as a colored band traveling down the fiber.
// Rather than `animatedBeam()`'s `userSpaceOnUse` + measured-pixel window
// (needed there because it connects two real DOM node positions),
// beams here use the SVG default `objectBoundingBox` gradient space — the
// window slides through `-0.3,-0.3` → `1.3,1.3` in the path's own 0–1
// bounding-box coordinates, so no `ResizeObserver`/`getBoundingClientRect`
// measuring is needed at all. The tradeoff (see `fidelityNotes`): the band
// travels along the bounding box's diagonal, not literally hugging every
// bend of a curved path — a fine approximation for gently-curved fibers.
//
// A single shared `requestAnimationFrame` loop drives every beam's gradient
// each frame (each beam keeps its own randomized duration/delay so they
// desync), gated by an `IntersectionObserver` that pauses the whole loop
// while the effect is scrolled out of view — the same perf idiom
// `flickeringGrid()` uses. A radial-gradient overlay div on top fades the
// beams out near the container's edges.
import type { DomphyElement, ElementNode, StyleObject } from "@domphy/core";
import { heading, paragraph } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
export interface BackgroundBeamsProps {
/** Custom SVG path `d` strings, overriding the default generated fibers. */
paths?: string[];
/** Number of default beams generated when `paths` is omitted. Capped for
* performance (the reference ships roughly 50; this defaults far lower).
* Defaults to `20`. */
count?: number;
/** Theme color roles cycled across each beam's traveling band (three
* consecutive roles per beam approximate a multi-hue "cyan-purple-magenta"
* band). Defaults to `["info", "primary", "secondary"]`. */
colors?: ThemeColor[];
/** Seconds per beam's full travel cycle (base value — actual per-beam
* duration is randomized around it so beams desync). Defaults to `8`. */
duration?: number;
/** Blur radius applied to every beam's stroke, in px. Defaults to `1.5`. */
blur?: number;
/** Toggles the radial-gradient edge-fade overlay. Defaults to `true`. */
showVignette?: boolean;
/** Foreground content layered above the beams. Defaults to a small demo heading. */
children?: DomphyElement | DomphyElement[];
/** Passthrough style merged onto the outer container. */
style?: StyleObject;
}
const VIEWBOX_WIDTH = 800;
const VIEWBOX_HEIGHT = 500;
let backgroundBeamsInstanceCounter = 0;
function randomBetween(min: number, max: number): number {
return min + Math.random() * (max - min);
}
/** One gently S-curved fiber running roughly top-to-bottom across the viewBox. */
function buildDefaultBeamPath(index: number, count: number): string {
const startX = (count <= 1 ? 0.5 : index / (count - 1)) * VIEWBOX_WIDTH * 1.3 - VIEWBOX_WIDTH * 0.15;
const startY = -30 - Math.random() * 40;
const endX = startX + randomBetween(-90, 90);
const endY = VIEWBOX_HEIGHT + 30 + Math.random() * 40;
const controlOneX = startX + randomBetween(-70, 70);
const controlOneY = VIEWBOX_HEIGHT * 0.33;
const controlTwoX = endX + randomBetween(-70, 70);
const controlTwoY = VIEWBOX_HEIGHT * 0.66;
return `M${startX.toFixed(1)} ${startY.toFixed(1)} C${controlOneX.toFixed(1)} ${controlOneY.toFixed(1)}, ${controlTwoX.toFixed(1)} ${controlTwoY.toFixed(1)}, ${endX.toFixed(1)} ${endY.toFixed(1)}`;
}
interface BeamRuntime {
gradientElement: SVGLinearGradientElement | null;
durationSeconds: number;
delaySeconds: number;
}
/**
* Full-bleed ambient background of many overlapping, independently traveling
* SVG light fibers on a dark backdrop — purely decorative, no pointer
* interaction. Call with no arguments for a working demo — twenty desynced
* beams behind a heading.
*/
function backgroundBeams(props: BackgroundBeamsProps = {}): DomphyElement<"div"> {
const instanceId = ++backgroundBeamsInstanceCounter;
const colors = props.colors && props.colors.length > 0 ? props.colors : (["info", "primary", "secondary"] as ThemeColor[]);
const count = Math.max(1, Math.round(props.count ?? 20));
const baseDuration = props.duration ?? 8;
const blur = props.blur ?? 1.5;
const showVignette = props.showVignette ?? true;
const filterId = `domphy-background-beams-blur-${instanceId}`;
const pathStrings = props.paths && props.paths.length > 0 ? props.paths : Array.from({ length: count }, (_unused, index) => buildDefaultBeamPath(index, count));
const runtimes: BeamRuntime[] = pathStrings.map(() => ({
gradientElement: null,
durationSeconds: baseDuration * randomBetween(0.7, 1.3),
delaySeconds: randomBetween(0, baseDuration),
}));
function gradientId(index: number): string {
return `domphy-background-beams-gradient-${instanceId}-${index}`;
}
// `<stop>` is a paint-server node, not text — it has no `color` to follow the
// tone context, so the `missing-color` doctor rule is a false positive here
// (mirrors animatedBeam.ts's own gradient stops).
function bandStop(offset: string, opacity: number, color: ThemeColor): DomphyElement {
return {
stop: null,
offset,
style: { stopColor: (listener) => themeColor(listener, "shift-10", color), stopOpacity: opacity } as StyleObject,
_doctorDisable: "missing-color",
} as DomphyElement;
}
function beamGradient(index: number): DomphyElement {
const colorA = colors[index % colors.length];
const colorB = colors[(index + 1) % colors.length];
const colorC = colors[(index + 2) % colors.length];
return {
linearGradient: [
bandStop("0%", 0, colorA),
bandStop("25%", 0.6, colorA),
bandStop("50%", 1, colorB),
bandStop("75%", 0.6, colorC),
bandStop("100%", 0, colorC),
],
id: gradientId(index),
// objectBoundingBox (SVG default) — no explicit x1/y1/x2/y2 attrs needed
// beyond the ones the rAF loop writes imperatively each frame.
_key: `gradient-${index}`,
_onMount: (node: ElementNode) => {
runtimes[index].gradientElement = node.domElement as unknown as SVGLinearGradientElement;
},
_onRemove: () => {
runtimes[index].gradientElement = null;
},
} as DomphyElement;
}
function beamPath(d: string, index: number): DomphyElement {
return {
path: null,
d,
fill: "none",
stroke: `url(#${gradientId(index)})`,
strokeWidth: "1.4",
strokeLinecap: "round",
_key: `beam-${index}`,
style: { filter: `url(#${filterId})` } as StyleObject,
} as DomphyElement;
}
const blurFilter: DomphyElement = {
filter: [{ feGaussianBlur: null, stdDeviation: String(blur) } as DomphyElement],
id: filterId,
x: "-50%",
y: "-50%",
width: "200%",
height: "200%",
} as DomphyElement;
const svgLayer: DomphyElement = {
svg: [
{ defs: [blurFilter, ...pathStrings.map((_unused, index) => beamGradient(index))] } as DomphyElement,
...pathStrings.map((d, index) => beamPath(d, index)),
],
viewBox: `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`,
preserveAspectRatio: "none",
ariaHidden: "true",
style: { position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none" } as StyleObject,
} as DomphyElement;
const vignetteOverlay: DomphyElement = {
div: null,
ariaHidden: "true",
// Decorative edge-fade overlay with no text of its own.
_doctorDisable: "missing-color",
style: {
position: "absolute",
inset: 0,
pointerEvents: "none",
backgroundImage: (listener) => `radial-gradient(ellipse at center, transparent 35%, ${themeColor(listener, "inherit")} 100%)`,
} as StyleObject,
} as DomphyElement;
const contentChildren: DomphyElement[] = props.children
? Array.isArray(props.children)
? props.children
: [props.children]
: [
{ h2: "Background Beams", $: [heading()] } as DomphyElement,
{
p: "A field of long, softly traveling light fibers behind your content.",
$: [paragraph()],
} as DomphyElement,
];
return {
div: [svgLayer, ...(showVignette ? [vignetteOverlay] : []), { div: contentChildren, style: { position: "relative", zIndex: 1 } } as DomphyElement],
dataTone: "shift-15",
_onMount: (node: ElementNode) => {
if (typeof window === "undefined") return;
const containerElement = node.domElement as HTMLElement;
let animationFrameId: number | null = null;
let intersectionObserver: IntersectionObserver | null = null;
function tick(timeMs: number): void {
// Belt-and-suspenders stop condition: some hosts (e.g. a test harness
// that wipes the DOM directly instead of going through the framework's
// removal lifecycle) never fire the "Remove" hook below, and this loop
// has no other convergence condition — it reschedules unconditionally
// every frame. Bailing here once the container is detached prevents
// it from leaking forever.
if (!containerElement.isConnected) return;
const timeSeconds = timeMs / 1000;
const bandHalf = 0.3;
for (const runtime of runtimes) {
const gradient = runtime.gradientElement;
if (!gradient) continue;
const elapsed = timeSeconds - runtime.delaySeconds;
if (elapsed < 0) continue;
const progress = (elapsed % runtime.durationSeconds) / runtime.durationSeconds;
const slide = -bandHalf + progress * (1 + bandHalf * 2);
gradient.setAttribute("x1", slide.toFixed(3));
gradient.setAttribute("y1", slide.toFixed(3));
gradient.setAttribute("x2", (slide + bandHalf).toFixed(3));
gradient.setAttribute("y2", (slide + bandHalf).toFixed(3));
}
animationFrameId = window.requestAnimationFrame(tick);
}
function startLoop(): void {
if (animationFrameId !== null) return;
animationFrameId = window.requestAnimationFrame(tick);
}
function stopLoop(): void {
if (animationFrameId === null) return;
window.cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (typeof IntersectionObserver === "function") {
intersectionObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) startLoop();
else stopLoop();
}
});
intersectionObserver.observe(containerElement);
} else {
startLoop();
}
node.addHook("Remove", () => {
stopLoop();
intersectionObserver?.disconnect();
});
},
style: {
position: "relative",
overflow: "hidden",
borderRadius: themeSpacing(4),
padding: themeSpacing(8),
minHeight: themeSpacing(80),
backgroundColor: (listener) => themeColor(listener, "inherit"),
color: (listener) => themeColor(listener, "shift-9"),
...(props.style ?? {}),
} as StyleObject,
};
}
export { backgroundBeams };