Domphy

neonGradientCard

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

Implementation notes

Three stacked layers in one wrapper: a blurred oversized glow copy of the two-color gradient behind everything, a sharp gradient frame that shows through exactly the padding gap the normal-flow content div leaves (no SVG/mask needed), and the content surface on top. Both gradient layers are pointer-events and their background-position loops via a CSS keyframe (alternating top-center/bottom-center) for the slow pulsing motion; hovering the wrapper intensifies the glow layer's opacity/blur via a [data-neon-glow]-scoped hover selector, matching the spec's hover-enhancement note. Default neonColors ('secondary'/'info') were chosen because those two families are magenta/pink and cyan hues in the default Domphy theme, matching the spec's 'hot pink/magenta paired with cyan' default description — theme tokens are used throughout instead of the literal hex values the upstream spec implies, per this package's color-token constraint.

Status: ported · Reference: Magic UI original

// Magic UI "Neon Gradient Card" — clean-room reimplementation.
//
// A card wrapped in a thick, saturated two-color gradient frame that reads
// like a neon sign outline, with a softer blurred duplicate behind it for
// the halo/glow. Implemented purely from the block's public functional/
// visual spec — no upstream Magic UI source was viewed or copied.
//
// Built as three stacked layers sharing one wrapper (padding creates the
// ring gap; no SVG/mask needed): a blurred, oversized glow copy of the
// gradient behind everything; a sharp gradient "frame" layer that shows
// through exactly the padding gap the ordinary-flow content div leaves; and
// the content surface itself, on top. Both gradient layers are
// `pointer-events: none` so normal interaction with the content is
// unaffected, and the frame's background-position is looped via a CSS
// keyframe for the "slow pulsing light" motion the spec describes.

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

export interface NeonGradientCardNeonColors {
  /** First gradient hue. Defaults to `"secondary"` (a magenta/pink family in the default theme). */
  firstColor?: ThemeColor;
  /** Second gradient hue. Defaults to `"info"` (a cyan family in the default theme). */
  secondColor?: ThemeColor;
}

export interface NeonGradientCardProps {
  /** Content rendered inside the frame. Defaults to a small demo card body. */
  children?: DomphyElement | DomphyElement[];
  /** Neon frame thickness, in `themeSpacing` units. Defaults to `5`. */
  borderSize?: number;
  /** Corner rounding, in pixels. Defaults to `20`. */
  borderRadius?: number;
  /** The two gradient hues the frame blends through. */
  neonColors?: NeonGradientCardNeonColors;
  /** Loop duration for the gradient's slow pulse, in seconds. Defaults to `6`. */
  duration?: number;
  /** Passthrough style merged onto the outer wrapper. */
  style?: StyleObject;
}

let neonGradientCardInstanceCounter = 0;

/**
 * A card framed by a thick, animated two-color neon gradient border with a
 * blurred halo behind it. Hovering intensifies the glow. Call with no
 * arguments for a working demo card.
 */
function neonGradientCard(props: NeonGradientCardProps = {}): DomphyElement<"div"> {
  const borderSize = props.borderSize ?? 5;
  const borderRadius = props.borderRadius ?? 20;
  const firstColor = props.neonColors?.firstColor ?? "secondary";
  const secondColor = props.neonColors?.secondColor ?? "info";
  const duration = props.duration ?? 6;
  const children: DomphyElement[] = props.children
    ? Array.isArray(props.children)
      ? props.children
      : [props.children]
    : [
        { h3: "Neon Gradient Card", $: [heading()] } as DomphyElement,
        {
          p: "A pulsing two-color neon frame halos this card, brightening further on hover.",
          $: [paragraph({ color: "neutral" })],
        } as DomphyElement,
      ];

  const instanceId = ++neonGradientCardInstanceCounter;
  const animationName = `neon-gradient-card-pulse-${hashString(
    JSON.stringify({ instanceId, firstColor, secondColor, duration }),
  )}`;
  // "background-position spin" — alternates the gradient's focal point
  // between top-center and bottom-center, looping forever.
  const keyframes = {
    "0%,100%": { backgroundPosition: "50% 0%" },
    "50%": { backgroundPosition: "50% 100%" },
  };

  const gradientImage = (listener: Listener) =>
    `linear-gradient(135deg, ${themeColor(listener, "shift-9", firstColor)}, ${themeColor(listener, "shift-9", secondColor)}, ${themeColor(listener, "shift-9", firstColor)})`;

  // Decorative gradient layers carry no text of their own — exempt from the
  // missing-color contract (same idiom as `borderBeam`/`shineBorder`'s ring
  // layers in this package). Built through untyped literals, then asserted,
  // so `_doctorDisable` (a doctor-only annotation not present in core's
  // strict `PartialElement` type) doesn't trip the excess-property check.
  const glowLayer = {
    div: null,
    dataNeonGlow: "true",
    ariaHidden: "true",
    _doctorDisable: "missing-color",
    style: {
      position: "absolute",
      inset: themeSpacing(-(borderSize * 2)),
      borderRadius: `${borderRadius + borderSize * 2}px`,
      backgroundImage: gradientImage,
      backgroundSize: "200% 200%",
      filter: `blur(${themeSpacing(borderSize * 3)})`,
      opacity: 0.5,
      pointerEvents: "none",
      zIndex: 0,
      transition: "opacity 300ms ease, filter 300ms ease",
      animation: `${animationName} ${duration}s ease-in-out infinite`,
      [`@keyframes ${animationName}`]: keyframes,
    } as StyleObject,
  } as DomphyElement<"div">;

  const frameLayer = {
    div: null,
    ariaHidden: "true",
    _doctorDisable: "missing-color",
    style: {
      position: "absolute",
      inset: 0,
      borderRadius: `${borderRadius}px`,
      backgroundImage: gradientImage,
      backgroundSize: "200% 200%",
      pointerEvents: "none",
      zIndex: 1,
      animation: `${animationName} ${duration}s ease-in-out infinite`,
      [`@keyframes ${animationName}`]: keyframes,
    } as StyleObject,
  } as DomphyElement<"div">;

  const contentLayer: DomphyElement<"div"> = {
    div: children,
    style: {
      position: "relative",
      zIndex: 2,
      borderRadius: `${Math.max(borderRadius - borderSize, 0)}px`,
      padding: themeSpacing(6),
      backgroundColor: (listener: Listener) => themeColor(listener, "inherit", "neutral"),
      color: (listener: Listener) => themeColor(listener, "shift-10", "neutral"),
    } as StyleObject,
  };

  return {
    div: [glowLayer, frameLayer, contentLayer],
    style: {
      position: "relative",
      borderRadius: `${borderRadius}px`,
      // The gap this padding leaves (between the wrapper's edge and the
      // ordinary-flow content div) is exactly where `frameLayer` — an
      // `inset: 0` absolutely positioned sibling filling the wrapper's whole
      // padding box — shows through as the visible neon ring.
      padding: themeSpacing(borderSize),
      "&:hover [data-neon-glow]": {
        opacity: 0.85,
        filter: `blur(${themeSpacing(borderSize * 2)})`,
      },
      ...(props.style ?? {}),
    } as StyleObject,
  };
}

export { neonGradientCard };
export type { NeonGradientCardNeonColors as NeonColors };

← Back to Magic UI catalog