Domphy

glareHover

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

Implementation notes

Fully functional clean-room port with two deliberate adaptations to Domphy's theme system: (1) the spec's literal hex/CSS glare color is instead a ThemeColor family (default "neutral") resolved via themeColor() at shift-11 off the container's own tone, since Domphy's doctor rules forbid raw hex/rgb colors on style props — this keeps the effect theme-aware (follows light/dark swaps) at the cost of an arbitrary caller hex; opacity is applied with CSS color-mix() rather than hex->rgba conversion. (2) The streak is a diagonal linear-gradient baked into backgroundImage, swept via an animated backgroundPosition (corner-to-corner) rather than translating/rotating a separate overlay element — visually equivalent, simpler CSS, and CSS-only per the animation guidance. playOnce is implemented with a JS animationend listener that flips a data-glare-armed attribute so a CSS :hover selector stops retriggering the keyframe after the first sweep; non-playOnce hovers replay every time since the CSS animation property is removed (not reversed) the instant :hover ends, snapping the band back to its off-canvas rest position instantly (the spec's "resets silently" option).

Status: ported · Reference: Magic UI original

// Magic UI "Glare Hover" — clean-room reimplementation.
//
// A wrapper that adds a soft diagonal light-streak sweeping across its child
// content on pointer-hover, mimicking a glare/reflection passing over a
// glossy surface. Implemented purely from the block's public
// functional/visual spec — no upstream Magic UI source was viewed or copied.
//
// The upstream spec asks for a literal hex/CSS glare color, but Domphy's
// doctor rules forbid raw hex/rgb colors on style props — so the color is
// expressed as a `ThemeColor` family instead, and the streak's alpha is
// applied with `color-mix()` (a CSS function, not a raw color literal) rather
// than converting a hex string to `rgba()`. This keeps the effect fully
// theme-aware (it now follows light/dark theme swaps) at the cost of not
// accepting an arbitrary caller-supplied hex value.

import type { DomphyElement, ElementNode, 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 GlareHoverProps {
  /** Content wrapped by the glare surface. Defaults to a small demo panel. */
  children?: DomphyElement | DomphyElement[];
  /** Theme color family for the streak. Defaults to `"neutral"` (a bright/white-reading sweep). */
  glareColor?: ThemeColor;
  /** Streak alpha, 0–1. Defaults to `0.35`. */
  glareOpacity?: number;
  /** Sweep angle in degrees. Defaults to `-45`. */
  angle?: number;
  /** Streak band size, as a percentage of the container's own box. Defaults to `220`. */
  size?: number;
  /** Sweep duration in ms. Defaults to `650`. */
  duration?: number;
  /** When true, the sweep only ever plays on the first pointer-enter. Defaults to `false`. */
  playOnce?: boolean;
  /** Edge-anchor surface tone for the container background. Defaults to `"dark"`. */
  surface?: "light" | "dark";
  /** Passthrough style merged onto the outer container. */
  style?: StyleObject;
}

let glareHoverInstanceCounter = 0;

/**
 * A wrapper that sweeps a soft diagonal light streak across its child content
 * on pointer-hover, mimicking a glare/reflection over a glossy surface. Call
 * with no arguments for a working demo — a dark panel that sweeps on hover.
 */
function glareHover(props: GlareHoverProps = {}): DomphyElement<"div"> {
  const instanceId = ++glareHoverInstanceCounter;
  const glareColor = props.glareColor ?? "neutral";
  const glareOpacity = props.glareOpacity ?? 0.35;
  const angle = props.angle ?? -45;
  const size = props.size ?? 220;
  const duration = props.duration ?? 650;
  const playOnce = props.playOnce ?? false;
  const surfaceTone = (props.surface ?? "dark") === "dark" ? "shift-15" : "shift-1";

  // The streak is a diagonal gradient band baked into `backgroundImage`, sized
  // larger than the box and swept via `backgroundPosition` — cheaper and
  // simpler than transforming/rotating a separate overlay element, and reads
  // identically as a soft light streak moving across the surface's diagonal.
  const keyframes = {
    from: { backgroundPosition: "-160% -160%" },
    to: { backgroundPosition: "160% 160%" },
  };
  const animationName = `glare-hover-sweep-${hashString(JSON.stringify({ keyframes, instanceId }))}`;

  const contentChildren: DomphyElement[] = props.children
    ? Array.isArray(props.children)
      ? props.children
      : [props.children]
    : [
        { h3: "Glare Hover", $: [heading()] } as DomphyElement,
        {
          p: "Hover to see the light sweep pass over this surface.",
          $: [paragraph()],
        } as DomphyElement,
      ];

  // `_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 fadeOverlay() in the
  // marquee block).
  const glareBand = {
    div: null,
    dataGlareBand: "true",
    ariaHidden: "true",
    // Decorative gradient streak with no text of its own — exempt from the
    // missing-color contract.
    _doctorDisable: "missing-color",
    _onMount: (node: ElementNode) => {
      if (!playOnce) return;
      const bandElement = node.domElement as HTMLElement;
      const containerElement = bandElement.parentElement;
      if (!containerElement) return;
      const onAnimationEnd = () => {
        containerElement.setAttribute("data-glare-armed", "false");
      };
      bandElement.addEventListener("animationend", onAnimationEnd);
      node.addHook("Remove", () => {
        bandElement.removeEventListener("animationend", onAnimationEnd);
      });
    },
    style: {
      position: "absolute",
      inset: 0,
      zIndex: 0,
      pointerEvents: "none",
      // shift-11 (not a small shift-1) so the streak reads as a bright
      // highlight against the container's own surface tone — a small shift
      // only nudges a couple of ramp steps toward the opposite edge and
      // would barely be distinguishable from the surrounding background.
      backgroundImage: (listener) =>
        `linear-gradient(${angle}deg, transparent 35%, color-mix(in srgb, ${themeColor(listener, "shift-11", glareColor)} ${Math.round(glareOpacity * 100)}%, transparent) 50%, transparent 65%)`,
      backgroundSize: `${size}% ${size}%`,
      backgroundRepeat: "no-repeat",
      backgroundPosition: "-160% -160%",
    } as StyleObject,
  } as DomphyElement<"div">;

  return {
    div: [
      { div: contentChildren, style: { position: "relative", zIndex: 1 } },
      glareBand,
    ],
    // Armed by default; playOnce disarms it (via the band's animationend
    // handler above) after the first sweep finishes.
    dataGlareArmed: "true",
    dataTone: surfaceTone,
    style: {
      position: "relative",
      overflow: "hidden",
      borderRadius: themeSpacing(4),
      padding: themeSpacing(6),
      backgroundColor: (listener) => themeColor(listener, "inherit"),
      color: (listener) => themeColor(listener, "shift-9"),
      "&[data-glare-armed=true]:hover [data-glare-band]": {
        animation: `${animationName} ${duration}ms linear forwards`,
      },
      [`@keyframes ${animationName}`]: keyframes,
      ...(props.style ?? {}),
    } as StyleObject,
  };
}

export { glareHover };

← Back to Magic UI catalog