spotlightDual
A Backgrounds block/component from Aceternity UI — clean-room reimplemented for Domphy (see methodology). Call spotlightDual() with no arguments for a working demo, or edit the code below live.
Implementation notes
Two mirrored groups of three stacked blurred radial-gradient ellipse layers (bright core / medium halo / faint outer glow), each group using the ui motion() patch for a one-shot mount opacity fade-in composed with an independent infinite CSS translateX sway keyframe (the two never conflict since motion() here only ever animates 'opacity', never 'transform'). Default color role is 'info' as a stand-in for the reference's fixed hue ~210 blue.
Status: ported · Reference: Aceternity UI original
// Aceternity UI "Spotlight New" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied).
// Two soft blue-toned radial spotlight glows anchored at the left and right
// edges of a dark hero section, each built from stacked blurred gradient
// layers for a smooth falloff, gently swaying for ambient depth.
//
// Two-stage animation, neither of which needs a JS loop: the `motion()`
// patch (Web Animations API) plays a one-shot fade-in (`opacity: 0 -> 1`) on
// mount, layered independently on top of a plain infinite CSS `@keyframes`
// that alternates each group's `transform: translateX(...)` back and forth —
// the two never conflict because `motion()` here only ever touches `opacity`
// (see its own source), leaving `transform` entirely to the CSS animation.
import type { DomphyElement, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { heading, motion, paragraph } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
export interface SpotlightDualProps {
/** Theme color role for the spotlight glow. Defaults to `"info"` (reads as a
* soft blue, matching the reference's hue ~210). */
color?: ThemeColor;
/** Main beam shape width, in `themeSpacing` units. Defaults to `70`. */
width?: number;
/** Main beam shape height, in `themeSpacing` units. Defaults to `170`. */
height?: number;
/** Secondary (narrower) beam layer width, in `themeSpacing` units. Defaults to `36`. */
smallWidth?: number;
/** Vertical offset applied to both beam groups, in `themeSpacing` units. Defaults to `-40`. */
translateY?: number;
/** Horizontal sway distance per cycle, in `themeSpacing` units. Defaults to `10`. */
xOffset?: number;
/** Seconds per sway cycle. Defaults to `7`. */
duration?: number;
/** Mount fade-in duration, in ms. Defaults to `1500`. */
fadeInDuration?: number;
/** Foreground content layered above the spotlights. Defaults to a small demo heading. */
children?: DomphyElement | DomphyElement[];
/** Passthrough style merged onto the outer container. */
style?: StyleObject;
}
/** One layer's falloff tuning: tone step (brightness), size multiplier, and blur. */
interface SpotlightLayerSpec {
tone: "shift-12" | "shift-9" | "shift-6";
sizeMultiplier: number;
blurPx: number;
opacity: number;
}
const LAYER_SPECS: SpotlightLayerSpec[] = [
{ tone: "shift-12", sizeMultiplier: 1, blurPx: 40, opacity: 0.9 }, // bright core
{ tone: "shift-9", sizeMultiplier: 1.5, blurPx: 70, opacity: 0.6 }, // medium halo
{ tone: "shift-6", sizeMultiplier: 2.1, blurPx: 110, opacity: 0.35 }, // faint outer glow
];
let spotlightDualInstanceCounter = 0;
/** One elongated, blurred radial-gradient ellipse — one falloff layer within a spotlight group. */
function spotlightLayer(
key: string,
spec: SpotlightLayerSpec,
color: ThemeColor,
width: number,
height: number,
): DomphyElement {
return {
div: null,
_key: key,
ariaHidden: "true",
// Decorative blurred glow with no text of its own — exempt from the
// missing-color contract (mirrors lightRays.ts's glow blobs).
_doctorDisable: "missing-color",
style: {
position: "absolute",
top: 0,
left: 0,
width: themeSpacing(Math.round(width * spec.sizeMultiplier)),
height: themeSpacing(Math.round(height * spec.sizeMultiplier)),
borderRadius: "50%",
opacity: spec.opacity,
filter: `blur(${spec.blurPx}px)`,
backgroundImage: (listener) => `radial-gradient(ellipse at center, ${themeColor(listener, spec.tone, color)} 0%, transparent 70%)`,
} as StyleObject,
} as DomphyElement;
}
/** One mirrored side's stack of falloff layers, swaying back and forth forever after its mount fade-in. */
function spotlightGroup(
side: "left" | "right",
instanceId: number,
color: ThemeColor,
width: number,
height: number,
smallWidth: number,
translateY: number,
xOffset: number,
duration: number,
fadeInDuration: number,
): DomphyElement {
const angle = side === "left" ? -45 : 45;
const swayDirection = side === "left" ? 1 : -1;
const swayKeyframes = {
"0%": { transform: `translateX(0) rotate(${angle}deg)` },
"50%": { transform: `translateX(${swayDirection * xOffset}px) rotate(${angle}deg)` },
"100%": { transform: `translateX(0) rotate(${angle}deg)` },
};
const swayAnimationName = `spotlight-dual-sway-${side}-${hashString(JSON.stringify({ instanceId, swayKeyframes }))}`;
return {
div: [
spotlightLayer("core", LAYER_SPECS[0], color, width, height),
spotlightLayer("halo", LAYER_SPECS[1], color, width, height),
spotlightLayer("outer", LAYER_SPECS[2], color, smallWidth, height),
],
_key: `spotlight-${side}`,
ariaHidden: "true",
$: [motion({ initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: fadeInDuration, easing: "ease-out" } })],
style: {
position: "absolute",
top: themeSpacing(translateY),
left: side === "left" ? themeSpacing(-10) : "auto",
right: side === "right" ? themeSpacing(-10) : "auto",
transformOrigin: side === "left" ? "top left" : "top right",
animation: `${swayAnimationName} ${duration}s ease-in-out infinite`,
[`@keyframes ${swayAnimationName}`]: swayKeyframes,
"@media (prefers-reduced-motion: reduce)": { animationPlayState: "paused" },
} as StyleObject,
} as DomphyElement;
}
/**
* Two mirrored, softly blurred spotlight glows anchored at the left and
* right edges of a dark hero section — fade in on mount, then sway gently
* forever. Purely ambient; no pointer interaction. Call with no arguments
* for a working demo — two blue spotlights behind a heading.
*/
function spotlightDual(props: SpotlightDualProps = {}): DomphyElement<"div"> {
const instanceId = ++spotlightDualInstanceCounter;
const color = props.color ?? "info";
const width = props.width ?? 70;
const height = props.height ?? 170;
const smallWidth = props.smallWidth ?? 36;
const translateY = props.translateY ?? -40;
const xOffset = props.xOffset ?? 10;
const duration = props.duration ?? 7;
const fadeInDuration = props.fadeInDuration ?? 1500;
const contentChildren: DomphyElement[] = props.children
? Array.isArray(props.children)
? props.children
: [props.children]
: defaultSpotlightContent();
return {
div: [
spotlightGroup("left", instanceId, color, width, height, smallWidth, translateY, xOffset, duration, fadeInDuration),
spotlightGroup("right", instanceId, color, width, height, smallWidth, translateY, xOffset, duration, fadeInDuration),
{ div: contentChildren, style: { position: "relative", zIndex: 1 } } as DomphyElement,
],
dataTone: "shift-15",
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,
};
}
function defaultSpotlightContent(): DomphyElement[] {
return [
{ h2: "Spotlight", $: [heading()] } as DomphyElement,
{
p: "Two soft blue glows fade in and sway gently behind this content.",
$: [paragraph()],
} as DomphyElement,
];
}
export { spotlightDual };