glowingEffect
A Backgrounds block/component from Aceternity UI — clean-room reimplemented for Domphy (see methodology). Call glowingEffect() with no arguments for a working demo, or edit the code below live.
Implementation notes
Reuses magicCard.ts's own content-box/border-box mask XOR ring technique, but drives a conic-gradient (swept by the angle from the card's center to the pointer) instead of a radial spotlight at the raw cursor position. Proximity radius (point-to-rect distance) and a central inactiveZone dead-zone (fraction of half-diagonal) both gate activation; a single document-level pointermove listener lets many instances react to the same global pointer, per the spec. Angle easing ('trails and catches up') is done via a manual per-frame circular lerp on a CSS custom property rather than a CSS transition, because animating a custom property's angle with transition requires the @property Houdini at-rule, which this codebase's CSS-in-JS style layer has no support for — noted as the one real fidelity gap. Also intentionally deviates from the reference's own default (disabled: true, opt-in only) by defaulting disabled to false so the zero-argument demo actually shows the effect for docs/screenshots; the prop still exists and behaves identically when set.
Status: partial · Reference: Aceternity UI original
// Aceternity UI "Glowing Effect" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied). An
// interactive border-glow overlay for a card: a thin illuminated arc that
// orbits the card's edge, tracking the pointer's angle relative to the
// card's center, only activating within a proximity radius and outside a
// central dead zone.
//
// The ring itself reuses `magicCard()`'s own `content-box`/`border-box`
// `mask` XOR trick already established in this package — a `padding`-sized
// gap between the two mask layers exposes only a border-width ring of the
// glow layer's `conic-gradient`. What's different here is the gradient
// itself: instead of a radial spotlight following the raw cursor position,
// a `conic-gradient(from <angle> at 50% 50%, ...)` sweeps a bright arc
// around the ring, and `<angle>` is the direction from the card's center to
// the pointer — not the cursor's literal (x, y). A single `document`-level
// `pointermove` listener (per spec, shared pointer state so many
// glow-wrapped cards can each react to the one global position) recomputes
// each instance's angle/proximity/dead-zone gating on every animation
// frame; the displayed angle chases the raw target with a manual circular
// lerp (shortest angular path, wrapping through 0/360) each
// `requestAnimationFrame` tick rather than a snap, giving the "trails and
// catches up" easing the spec calls for — plain CSS can't `transition` a
// custom property's angle without the `@property` Houdini at-rule, which
// this codebase's CSS-in-JS layer has no support for, so the easing is done
// in JS instead (see `fidelityNotes`).
import type { DomphyElement, ElementNode, StyleObject } from "@domphy/core";
import { heading, paragraph } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
export interface GlowingEffectProps {
/** `"default"` sweeps a multi-color arc; `"white"` is a monochrome neutral glow. Defaults to `"default"`. */
variant?: "default" | "white";
/** Theme color roles cycled across the multi-color arc, `"default"` variant only.
* Defaults to `["info", "primary", "secondary"]`. */
colors?: ThemeColor[];
/** Blur radius applied to the glow ring, in px. Defaults to `8`. */
blur?: number;
/** Central dead-zone size as a fraction (0–1) of the card's own half-diagonal —
* pointer positions inside this radius from center never trigger the glow, even
* directly over the card's middle. Defaults to `0.6`. */
inactiveZone?: number;
/** How far outside the card's edge (in px) the effect still triggers. Defaults to `80`. */
proximity?: number;
/** Angular width of the bright arc, in degrees. Defaults to `90`. */
spread?: number;
/** Ring thickness, in `themeSpacing` units. Defaults to `1`. */
borderWidth?: number;
/** Forces the glow to show at a fixed angle without any pointer tracking. Defaults to `false`. */
alwaysOn?: boolean;
/** Turns off all pointer interactivity — the ring renders but never lights up (unless `alwaysOn`).
* The reference component defaults this to `true`; this factory defaults it to `false` instead so
* calling it with no arguments still demonstrates the effect (see `fidelityNotes`). */
disabled?: boolean;
/** Corner radius, in `themeSpacing` units. Defaults to `4`. */
borderRadius?: number;
/** Card content wrapped by the glow. Defaults to a small demo card body. */
children?: DomphyElement | DomphyElement[];
/** Passthrough style merged onto the outer wrapper. */
style?: StyleObject;
}
/** Shortest-path circular lerp toward `target` (degrees), wrapping through 0/360. */
function lerpAngle(current: number, target: number, factor: number): number {
let delta = ((target - current + 540) % 360) - 180;
return current + delta * factor;
}
/** Distance (px) from `(pointX, pointY)` to the nearest edge of `rect`; `0` when the point is inside. */
function distanceToRect(pointX: number, pointY: number, rect: DOMRect): number {
const clampedX = Math.max(rect.left, Math.min(pointX, rect.right));
const clampedY = Math.max(rect.top, Math.min(pointY, rect.bottom));
return Math.hypot(pointX - clampedX, pointY - clampedY);
}
function defaultGlowingContent(): DomphyElement[] {
return [
{ h3: "Glowing Effect", $: [heading()] } as DomphyElement,
{
p: "Move your cursor near this card — a soft light arc orbits its border, tracking the pointer's angle.",
$: [paragraph({ color: "neutral" })],
} as DomphyElement,
];
}
/**
* A border-hugging glow ring that orbits a card's edge, tracking the angle
* from the card's center to the pointer, gated by a proximity radius and a
* central dead zone. Call with no arguments for a working demo card — move
* the pointer near it to see the arc track.
*/
function glowingEffect(props: GlowingEffectProps = {}): DomphyElement<"div"> {
const variant = props.variant ?? "default";
const colors = props.colors && props.colors.length > 0 ? props.colors : (["info", "primary", "secondary"] as ThemeColor[]);
const blur = props.blur ?? 8;
const inactiveZone = props.inactiveZone ?? 0.6;
const proximity = props.proximity ?? 80;
const spread = props.spread ?? 90;
const borderWidth = props.borderWidth ?? 1;
const alwaysOn = props.alwaysOn ?? false;
const disabled = props.disabled ?? false;
const borderRadius = props.borderRadius ?? 4;
const contentChildren: DomphyElement[] = props.children
? Array.isArray(props.children)
? props.children
: [props.children]
: defaultGlowingContent();
const arcStops =
variant === "white"
? (["neutral", "neutral", "neutral"] as ThemeColor[])
: [colors[0], colors[1 % colors.length], colors[2 % colors.length]];
const glowLayer: DomphyElement = {
div: null,
ariaHidden: "true",
// Decorative border-glow ring with no text of its own — exempt from the
// missing-color contract (mirrors magicCard.ts's own border-glow layer).
_doctorDisable: "missing-color",
_onMount: (node: ElementNode) => {
const glowElement = node.domElement as HTMLElement;
const wrapperElement = glowElement.parentElement as HTMLElement | null;
if (!wrapperElement || typeof window === "undefined") return;
if (disabled && !alwaysOn) return;
let displayedAngle = 0;
let targetAngle = 0;
let targetOpacity = alwaysOn ? 1 : 0;
let displayedOpacity = alwaysOn ? 1 : 0;
let animationFrameId: number | null = null;
function paint(): void {
glowElement.style.setProperty("--glowing-effect-angle", `${displayedAngle}deg`);
glowElement.style.opacity = String(displayedOpacity);
}
paint();
function step(): 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. Bailing here
// once the node is detached prevents the document-level `pointermove`
// listener from resurrecting this loop forever on every later mouse move.
if (!glowElement.isConnected) return;
displayedAngle = lerpAngle(displayedAngle, targetAngle, 0.15);
displayedOpacity += (targetOpacity - displayedOpacity) * 0.15;
paint();
const settled = Math.abs(displayedOpacity - targetOpacity) < 0.01 && Math.abs(((targetAngle - displayedAngle + 540) % 360) - 180) < 0.5;
animationFrameId = settled ? null : window.requestAnimationFrame(step);
}
function ensureLoopRunning(): void {
if (animationFrameId === null) animationFrameId = window.requestAnimationFrame(step);
}
let removePointerListener: (() => void) | null = null;
if (!alwaysOn) {
const onPointerMove = (event: PointerEvent) => {
const rect = wrapperElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = event.clientX - centerX;
const deltaY = event.clientY - centerY;
const distanceFromCenter = Math.hypot(deltaX, deltaY);
const halfDiagonal = Math.hypot(rect.width / 2, rect.height / 2);
const edgeDistance = distanceToRect(event.clientX, event.clientY, rect);
const withinProximity = edgeDistance <= proximity;
const inDeadZone = distanceFromCenter < halfDiagonal * inactiveZone;
const active = withinProximity && !inDeadZone;
targetOpacity = active ? 1 : 0;
if (active) {
targetAngle = ((Math.atan2(deltaY, deltaX) * 180) / Math.PI + 90 + 360) % 360;
}
ensureLoopRunning();
};
document.addEventListener("pointermove", onPointerMove, { passive: true });
removePointerListener = () => document.removeEventListener("pointermove", onPointerMove);
}
node.addHook("Remove", () => {
if (animationFrameId !== null) window.cancelAnimationFrame(animationFrameId);
removePointerListener?.();
});
},
style: {
position: "absolute",
inset: 0,
padding: themeSpacing(borderWidth),
boxSizing: "border-box",
borderRadius: themeSpacing(borderRadius),
opacity: alwaysOn ? 1 : 0,
pointerEvents: "none",
filter: `blur(${blur}px)`,
backgroundImage: (listener) =>
`conic-gradient(from var(--glowing-effect-angle, 0deg) at 50% 50%, transparent 0deg, ${themeColor(listener, "shift-11", arcStops[0])} ${spread * 0.5}deg, ${themeColor(listener, "shift-9", arcStops[1])} ${spread}deg, ${themeColor(listener, "shift-11", arcStops[2])} ${spread * 1.5}deg, transparent ${spread * 2}deg, transparent 360deg)`,
WebkitMask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
WebkitMaskComposite: "xor",
mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
maskComposite: "exclude",
transition: "opacity 200ms ease",
} as StyleObject,
} as DomphyElement;
return {
div: [
glowLayer,
{
div: contentChildren,
style: { position: "relative", zIndex: 1, padding: themeSpacing(6) } as StyleObject,
} as DomphyElement,
],
style: {
position: "relative",
overflow: "hidden",
borderRadius: themeSpacing(borderRadius),
backgroundColor: (listener) => themeColor(listener, "inherit", "neutral"),
color: (listener) => themeColor(listener, "shift-10", "neutral"),
outline: (listener) => `1px solid ${themeColor(listener, "shift-3", "neutral")}`,
outlineOffset: "-1px",
...(props.style ?? {}),
} as StyleObject,
};
}
export { glowingEffect };