Domphy

heroHighlight

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

Implementation notes

Two-layer pure-CSS dot grid (background-image radial-gradient tiling) matches the spec's domSketch exactly: a faint base layer plus a brighter overlay layer masked to a soft circle bound to two CSS custom properties (--hero-highlight-x/-y) written imperatively on pointermove for 1, no-easing cursor tracking -- no animation loop needed, as the spec itself calls for. Also exports a secondary heroHighlightMark(props) sub-piece (children/className/color/sweepDuration) for the marked-phrase highlighter bar, which motion()-tweens width 0%->100% once on mount (a one-shot sweep, never replayed since no hover listener is attached) -- this satisfies the spec's own two-piece prop table (HeroHighlight wrapper + Highlight sub-piece) even though the task listed only 'heroHighlight' as the exportName. Exact dot color/opacity and the highlighter's accent color are this implementation's own reasonable defaults (light neutral dot grid, warm 'warning'-role accent bar) since the upstream demo was client-rendered and not pixel-inspectable, per the task's own researchNote -- low-to-moderate confidence on precise color tokens, high confidence on the two-layer spotlight + one-shot marker-sweep mechanism.

Status: ported · Reference: Aceternity UI original

// Aceternity UI "Hero Highlight" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied). A
// full-bleed hero background made of a faint dot grid whose dots light up in
// a soft radius that follows the mouse, paired with a headline where marked
// phrases get an animated highlighter-marker bar swept in behind them.
//
// Two dot-grid layers, both pure CSS `background-image` tiling (a single
// `radial-gradient(circle at 1px 1px, color 1px, transparent 0)` repeated via
// `background-size`) rather than an SVG/canvas grid — no per-dot DOM nodes or
// draw loop are needed here since neither layer needs independent per-dot
// timing (unlike this package's own `dotPattern.ts`/`dottedGlowBackground.ts`).
// The second (overlay) layer is masked to a soft circle bound to two CSS
// custom properties (`--hero-highlight-x/-y`, percentages) that are written
// straight to the DOM on every `pointermove` — pure CSS reacting to those
// custom properties, no animation loop, no easing, 1:1 cursor tracking. This
// is the same "write custom properties imperatively on `mousemove`, read them
// back inside a `radial-gradient`" technique `evervaultCard.ts` uses for its
// own hover spotlight elsewhere in this package.
//
// The highlighter marker (`heroHighlightMark`) is a small colored bar behind
// the marked phrase, absolutely positioned in its own wrapper span and
// animated from `width: 0%` to `100%` once via `motion()` on mount — a
// one-shot sweep, never replayed (no hover listener is ever attached to it).
//
// FIDELITY NOTE (per the task's own researchNote): the exact dot color/
// opacity and the highlighter's accent color could not be pixel-verified
// from the docs-only source (client-rendered demo, only the props table and
// tags were retrievable) — implemented with a light neutral dot grid and a
// warm accent marker as a reasonable default, per the task's own guidance.

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

export interface HeroHighlightProps {
  /** Hero content rendered above the dot-grid/spotlight background. Defaults to a short demo headline. */
  children?: DomphyElement | DomphyElement[];
  /** Extra class name merged onto the outer section's native `class` attribute. */
  containerClassName?: string;
  /** Extra class name merged onto the inner content wrapper's native `class` attribute. */
  className?: string;
  /** Grid gap between dots, in px. Defaults to `22`. */
  dotSpacing?: number;
  /** Theme color family for the faint base dot grid. Defaults to `"neutral"`. */
  dotColor?: ThemeColor;
  /** Theme color family for the brighter spotlight-revealed dot grid. Defaults to `"primary"`. */
  spotlightColor?: ThemeColor;
  /** Spotlight circle radius, in px. Defaults to `220`. */
  spotlightRadius?: number;
  /** Passthrough style merged onto the outer section. */
  style?: StyleObject;
}

export interface HeroHighlightMarkProps {
  /** The word/phrase marked by the highlighter bar. Defaults to an empty string. */
  children?: string;
  /** Extra class name merged onto the marker wrapper's native `class` attribute. */
  className?: string;
  /** Theme color family for the marker bar. Defaults to `"warning"` (reads as a warm accent). */
  color?: ThemeColor;
  /** Milliseconds the left-to-right sweep takes to reach full width. Defaults to `1800`. */
  sweepDuration?: number;
  /** Passthrough style merged onto the marker wrapper. */
  style?: StyleObject;
}

const MOUSE_X_PROPERTY = "--hero-highlight-x";
const MOUSE_Y_PROPERTY = "--hero-highlight-y";

/**
 * The colored marker bar behind a highlighted word/phrase — sweeps in from
 * zero width to full width once, on mount, like a highlighter pen dragged
 * across the text. Meant to be nested inside a `heroHighlight()` headline
 * (or any other text).
 */
function heroHighlightMark(props: HeroHighlightMarkProps = {}): DomphyElement<"span"> {
  const text = props.children ?? "";
  const color = props.color ?? "warning";
  const sweepDurationMs = Math.max(200, props.sweepDuration ?? 1800);

  // `_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
  // `dottedGlowBackground.ts`/`flickeringGrid.ts`).
  const barElement = {
    span: null,
    ariaHidden: "true",
    // Decorative marker bar with no text of its own — exempt from the
    // missing-color contract, matching this package's other purely
    // decorative glow/accent elements (e.g. `spotlightDual.ts`'s layers).
    // Also exempt from tone-background-inherit: the marker's fixed accent
    // color is intentional, not a surface (same reasoning `glowingStars.ts`/
    // `shootingStars.ts` document for their own decorative accents).
    _doctorDisable: ["missing-color", "tone-background-inherit"],
    style: {
      position: "absolute",
      insetInlineStart: themeSpacing(-1),
      bottom: "0.05em",
      width: "0%",
      height: "0.4em",
      borderRadius: themeSpacing(0.5),
      zIndex: 0,
      backgroundColor: (listener: Listener) => themeColor(listener, "shift-9", color),
    } as StyleObject,
    $: [motion({ initial: { width: "0%" }, animate: { width: "100%" }, transition: { duration: sweepDurationMs, easing: "ease-out" } })],
  } as DomphyElement<"span">;

  return {
    span: [barElement, { span: text, style: { position: "relative", zIndex: 1 } } as DomphyElement],
    class: props.className,
    style: { position: "relative", display: "inline-block", ...(props.style ?? {}) } as StyleObject,
  };
}

function defaultHeroHighlightContent(): DomphyElement[] {
  return [
    {
      h1: ["Design interfaces your users will ", heroHighlightMark({ children: "actually love" }), "."],
      $: [heading()],
    } as DomphyElement,
    {
      p: "Move your cursor around — the dot grid lights up wherever you point.",
      $: [paragraph()],
    } as DomphyElement,
  ];
}

/**
 * A full-bleed hero background: a faint dot grid whose dots light up in a
 * soft radius that follows the cursor 1:1, with headline content (typically
 * including one or more `heroHighlightMark()` phrases) layered on top. Call
 * with no arguments for a working demo.
 */
function heroHighlight(props: HeroHighlightProps = {}): DomphyElement<"section"> {
  const dotSpacing = Math.max(6, Math.round(props.dotSpacing ?? 22));
  const dotColor = props.dotColor ?? "neutral";
  const spotlightColor = props.spotlightColor ?? "primary";
  const spotlightRadius = Math.max(40, props.spotlightRadius ?? 220);

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

  let spotlightLayerElement: HTMLElement | null = null;

  const baseDotLayer = {
    div: null,
    ariaHidden: "true",
    _doctorDisable: "missing-color",
    style: {
      position: "absolute",
      inset: 0,
      backgroundImage: (listener: Listener) => `radial-gradient(circle at 1px 1px, ${themeColor(listener, "shift-4", dotColor)} 1px, transparent 0)`,
      backgroundSize: `${dotSpacing}px ${dotSpacing}px`,
    } as StyleObject,
  } as DomphyElement<"div">;

  const spotlightDotLayer = {
    div: null,
    ariaHidden: "true",
    _doctorDisable: "missing-color",
    _onMount: (node: ElementNode) => {
      spotlightLayerElement = node.domElement as HTMLElement;
    },
    _onRemove: () => {
      spotlightLayerElement = null;
    },
    style: {
      position: "absolute",
      inset: 0,
      opacity: 0,
      transition: "opacity 200ms ease",
      backgroundImage: (listener: Listener) => `radial-gradient(circle at 1px 1px, ${themeColor(listener, "shift-9", spotlightColor)} 1px, transparent 0)`,
      backgroundSize: `${dotSpacing}px ${dotSpacing}px`,
      maskImage: `radial-gradient(circle ${spotlightRadius}px at var(${MOUSE_X_PROPERTY}, 50%) var(${MOUSE_Y_PROPERTY}, 50%), black, transparent 80%)`,
      WebkitMaskImage: `radial-gradient(circle ${spotlightRadius}px at var(${MOUSE_X_PROPERTY}, 50%) var(${MOUSE_Y_PROPERTY}, 50%), black, transparent 80%)`,
    } as StyleObject,
  } as DomphyElement<"div">;

  const contentWrapper: DomphyElement<"div"> = {
    div: contentChildren,
    class: props.className,
    style: {
      position: "relative",
      zIndex: 1,
      display: "flex",
      flexDirection: "column",
      alignItems: "center",
      textAlign: "center",
      gap: themeSpacing(4),
      maxWidth: "42em",
      marginInline: "auto",
    } as StyleObject,
  };

  return {
    section: [baseDotLayer, spotlightDotLayer, contentWrapper],
    class: props.containerClassName,
    style: {
      position: "relative",
      overflow: "hidden",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      borderRadius: themeSpacing(4),
      padding: themeSpacing(10),
      minHeight: themeSpacing(96),
      backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
      color: (listener: Listener) => themeColor(listener, "shift-9"),
      ...(props.style ?? {}),
    } as StyleObject,
    _onMount: (node: ElementNode) => {
      if (typeof window === "undefined") return;
      const sectionElement = node.domElement as HTMLElement;
      sectionElement.style.setProperty(MOUSE_X_PROPERTY, "50%");
      sectionElement.style.setProperty(MOUSE_Y_PROPERTY, "50%");

      const handlePointerMove = (event: MouseEvent) => {
        const rect = sectionElement.getBoundingClientRect();
        if (rect.width === 0 || rect.height === 0) return;
        const xPercent = ((event.clientX - rect.left) / rect.width) * 100;
        const yPercent = ((event.clientY - rect.top) / rect.height) * 100;
        sectionElement.style.setProperty(MOUSE_X_PROPERTY, `${xPercent}%`);
        sectionElement.style.setProperty(MOUSE_Y_PROPERTY, `${yPercent}%`);
      };
      const handlePointerEnter = () => {
        if (spotlightLayerElement) spotlightLayerElement.style.opacity = "1";
      };
      const handlePointerLeave = () => {
        if (spotlightLayerElement) spotlightLayerElement.style.opacity = "0";
      };

      sectionElement.addEventListener("pointermove", handlePointerMove);
      sectionElement.addEventListener("pointerenter", handlePointerEnter);
      sectionElement.addEventListener("pointerleave", handlePointerLeave);

      node.addHook("Remove", () => {
        sectionElement.removeEventListener("pointermove", handlePointerMove);
        sectionElement.removeEventListener("pointerenter", handlePointerEnter);
        sectionElement.removeEventListener("pointerleave", handlePointerLeave);
      });
    },
  } as DomphyElement<"section">;
}

export { heroHighlight, heroHighlightMark };

← Back to Aceternity UI catalog