Domphy

backgroundGradient

A Backgrounds block/component from Aceternity UI — clean-room reimplemented for Domphy (see methodology). Call backgroundGradient() with no arguments for a working demo, or edit the code below live.

Implementation notes

Oversized, heavily blurred backgroundImage gradient layer positioned behind an opaque content wrapper (default demo wraps a card() patch), with a single animate boolean toggling a background-position-panning @keyframes loop on/off exactly as the spec's researchNote describes. Gradient stops use Domphy theme color roles (success/secondary/info/highlight by default, caller-overridable) instead of literal hex values, matching the framework's no-raw-color constraint.

Status: ported · Reference: Aceternity UI original

// Aceternity UI "Background Gradient" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied). A
// soft, blurred, animated color-gradient glow sitting directly behind a card
// or panel, reading as a glowing halo/border that bleeds out around the
// wrapped content's edges.
//
// Layered-blur technique: an absolutely-positioned layer, sized larger than
// (and behind) the wrapped content, carries a multi-stop `backgroundImage`
// gradient blending several theme color roles plus a heavy `blur()` filter
// — the content itself sits in a separate, opaque wrapper stacked on top via
// `z-index` so text stays legible. The gradient uses `backgroundImage` (not
// `backgroundColor`) specifically so `tone-background-inherit` — which only
// inspects the `backgroundColor` prop — never applies to it; this mirrors
// `warpBackground.ts`'s own grid-line gradients.
//
// `animate` (default `true`) toggles a single `@keyframes` loop that slowly
// pans the oversized gradient's `background-position` back and forth,
// reading as the colors slowly drifting/swirling; when `false` the same
// layer renders once with a fixed position and no `animation` property at
// all — the spec's own static/animated toggle contract.

import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { card, heading, paragraph } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";

export interface BackgroundGradientProps {
  /** Content wrapped by the glow — typically a card. Defaults to a small demo card. */
  children?: DomphyElement | DomphyElement[];
  /** Continuously animates the glow's gradient position on an endless loop. When
   * `false`, the glow renders as a fixed, static colorful blur. Defaults to `true`. */
  animate?: boolean;
  /** Theme color roles blended into the glow gradient, in stop order. Defaults to
   * a four-hue mix (`success`, `secondary`, `info`, `highlight`). */
  glowColors?: ThemeColor[];
  /** Blur radius applied to the glow layer, in `themeSpacing` units. Defaults to `24`. */
  blurRadius?: number;
  /** One drift cycle, in seconds. Defaults to `6`. */
  duration?: number;
  /** Passthrough style merged onto the content wrapper (the card's own background/border/radius). */
  contentStyle?: StyleObject;
  /** Passthrough style merged onto the outer container (sizing/margin). */
  style?: StyleObject;
}

const DEFAULT_GLOW_COLORS: ThemeColor[] = ["success", "secondary", "info", "highlight"];

let backgroundGradientInstanceCounter = 0;

function defaultGradientContent(): DomphyElement[] {
  return [
    {
      div: [
        { h3: "Background Gradient", $: [heading()] } as DomphyElement,
        {
          p: "A soft, colorful glow drifts behind this card, blurred into a glowing halo.",
          $: [paragraph()],
        } as DomphyElement,
      ],
      $: [card({ color: "neutral" })],
      style: { maxWidth: themeSpacing(72) },
    } as DomphyElement,
  ];
}

/**
 * Wraps content (typically a card) with a soft, blurred, colorful gradient
 * glow bleeding out around its edges — continuously drifting by default, or
 * a fixed static blur when `animate` is `false`. Call with no arguments for
 * a working demo — a small card with a slowly shifting rainbow halo.
 */
function backgroundGradient(props: BackgroundGradientProps = {}): DomphyElement<"div"> {
  const instanceId = ++backgroundGradientInstanceCounter;
  const animate = props.animate ?? true;
  const glowColors = props.glowColors && props.glowColors.length > 0 ? props.glowColors : DEFAULT_GLOW_COLORS;
  const blurRadius = props.blurRadius ?? 24;
  const duration = props.duration ?? 6;

  const driftAnimationName = `background-gradient-drift-${hashString(
    JSON.stringify({ instanceId, duration }),
  )}`;
  const driftKeyframes = {
    "0%,100%": { backgroundPosition: "0% 50%" },
    "50%": { backgroundPosition: "100% 50%" },
  };

  const contentChildren = props.children
    ? Array.isArray(props.children)
      ? props.children
      : [props.children]
    : defaultGradientContent();

  // `_doctorDisable` is a doctor-only annotation not present in core's strict
  // `PartialElement` type — build through an untyped literal, then assert, so
  // the excess-property check doesn't fire (mirrors warpBackground.ts).
  const glowLayer = {
    div: null,
    ariaHidden: "true",
    // Decorative glow layer with no text of its own — exempt from the
    // missing-color contract, matching warpBackground.ts's plane grid-lines.
    _doctorDisable: "missing-color",
    style: {
      position: "absolute",
      inset: themeSpacing(-4),
      borderRadius: themeSpacing(6),
      backgroundImage: (listener: Listener) =>
        `linear-gradient(115deg, ${glowColors
          .map((color) => themeColor(listener, "shift-9", color))
          .join(", ")})`,
      backgroundSize: "300% 300%",
      filter: `blur(${themeSpacing(blurRadius)})`,
      opacity: 0.85,
      zIndex: 0,
      ...(animate
        ? {
            animation: `${driftAnimationName} ${duration}s ease infinite`,
            [`@keyframes ${driftAnimationName}`]: driftKeyframes,
          }
        : { backgroundPosition: "50% 50%" }),
    } as StyleObject,
  } as DomphyElement<"div">;

  return {
    div: [
      glowLayer,
      {
        div: contentChildren,
        style: {
          position: "relative",
          zIndex: 1,
          ...(props.contentStyle ?? {}),
        } as StyleObject,
      } as DomphyElement<"div">,
    ],
    style: {
      position: "relative",
      display: "inline-block",
      ...(props.style ?? {}),
    } as StyleObject,
  };
}

export { backgroundGradient };

← Back to Aceternity UI catalog