Domphy

evervaultCard

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

Implementation notes

Full implementation: persistent title, 4 corner plus marks, a muted monospace character grid that continuously reshuffles a random subset of cells on an interval (independent of the mouse), and a cursor-tracked colorful spotlight. One implementation-choice note: the spotlight reveal is done with a single character layer plus a mix-blend-mode: color gradient blob layered above it (isolated in its own stacking context so it never bleeds onto the title), rather than a literal second full-opacity colorful text layer masked in — visually equivalent 'decrypt in focus under the cursor' result with half the DOM nodes and no extra per-cell state.

Status: ported · Reference: Aceternity UI original

// Aceternity UI "Evervault Card" — clean-room reimplementation from the
// public behavior/visual spec only (no upstream source viewed or copied).
// A hover-reactive card whose background is a dense field of random
// characters that visually "decrypts" into a colorful reveal wherever the
// cursor hovers, with a persistent title layered on top.
//
// The character grid is a single static-looking layer (muted, low-contrast
// text) that a periodic `setInterval` re-randomizes a small random subset
// of every tick — the same imperative-DOM-write "swap `textContent` on
// captured refs" idiom `hyperText.ts` uses for its scramble loop, just
// running forever instead of resolving.
//
// The "decrypt spotlight" itself needs no second character layer or JS
// per-frame work: a blurred, colorful radial-gradient blob is layered
// *above* the character grid with `mix-blend-mode: color`, so wherever it
// overlaps the muted text it recolors those glyphs in place. The blob's
// position is written straight to CSS custom properties on `mousemove`
// (same 1:1, no-easing tracking `magicCard.ts` uses) and both the grid and
// the blob live inside their own `isolation: isolate` group so the blend
// mode never bleeds onto the title layer stacked on top of it.

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

export interface EvervaultCardProps {
  /** Persistent title/label text, always legible above the character field. Defaults to `"Hover me"`. */
  title?: string;
  /** Character pool the noise grid is drawn from. Defaults to alphanumerics + a few symbols. */
  characters?: string;
  /** Grid column count. Defaults to `22`. */
  columns?: number;
  /** Grid row count. Defaults to `13`. */
  rows?: number;
  /** Spotlight diameter, in px. Defaults to `260`. */
  spotlightSize?: number;
  /** How often (ms) a random subset of characters re-rolls. Defaults to `140`. */
  shuffleIntervalMs?: number;
  /** Fraction (0-1) of cells re-rolled per shuffle tick. Defaults to `0.05`. */
  shuffleFraction?: number;
  /** Passthrough style merged onto the outer card. */
  style?: StyleObject;
}

const DEFAULT_CHARACTER_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";

function randomCharacter(pool: string): string {
  return pool.charAt(Math.floor(Math.random() * pool.length));
}

function cornerPlus(cornerStyle: StyleObject): DomphyElement<"span"> {
  return {
    span: "+",
    ariaHidden: "true",
    style: {
      position: "absolute",
      color: (listener: Listener) => themeColor(listener, "shift-6"),
      fontSize: (listener: Listener) => themeSize(listener, "decrease-1"),
      // No lineHeight override needed — this is a single absolutely-positioned
      // decorative glyph, not a text block, so the browser's default line box
      // doesn't affect legibility or layout here.
      userSelect: "none",
      ...cornerStyle,
    } as StyleObject,
  } as DomphyElement<"span">;
}

let evervaultCardInstanceCounter = 0;

/**
 * A hover-reactive card whose background is a dense field of random
 * characters that recolors into focus wherever the cursor hovers, with a
 * persistent title layered on top. Call with no arguments for a working
 * demo card.
 */
function evervaultCard(props: EvervaultCardProps = {}): DomphyElement<"div"> {
  const instanceId = ++evervaultCardInstanceCounter;
  const title = props.title ?? "Hover me";
  const characterPool = props.characters ?? DEFAULT_CHARACTER_POOL;
  const columns = Math.max(2, Math.round(props.columns ?? 22));
  const rows = Math.max(2, Math.round(props.rows ?? 13));
  const spotlightSize = props.spotlightSize ?? 260;
  const shuffleIntervalMs = props.shuffleIntervalMs ?? 140;
  const shuffleFraction = Math.min(1, Math.max(0, props.shuffleFraction ?? 0.05));

  const totalCells = columns * rows;
  const characterElementRefs: (HTMLElement | null)[] = new Array(totalCells).fill(null);

  const cells: DomphyElement<"span">[] = [];
  for (let index = 0; index < totalCells; index += 1) {
    cells.push({
      span: randomCharacter(characterPool),
      _key: `evervault-cell-${instanceId}-${index}`,
      _onMount: (node: ElementNode) => {
        characterElementRefs[index] = node.domElement as HTMLElement;
      },
      _onRemove: () => {
        characterElementRefs[index] = null;
      },
      style: {
        display: "inline-flex",
        alignItems: "center",
        justifyContent: "center",
        userSelect: "none",
      } as StyleObject,
    } as DomphyElement<"span">);
  }

  let spotlightElement: HTMLElement | null = null;

  const characterGrid: DomphyElement<"div"> = {
    div: cells,
    ariaHidden: "true",
    // Monospace alignment is fundamental to this grid's uniform character
    // cells, and `@domphy/theme` has no font-family token (AGENTS.md:
    // "fontFamily -> remove entirely, theme owns the font stack") — there
    // is no theme-native way to request a fixed-width face, so this one
    // property pair (fontFamily/lineHeight) is a narrow, documented
    // exception, same as `asciiArt.ts`'s grid.
    _doctorDisable: "inline-typography",
    style: {
      position: "absolute",
      inset: 0,
      display: "grid",
      gridTemplateColumns: `repeat(${columns}, 1fr)`,
      alignContent: "center",
      justifyItems: "center",
      overflow: "hidden",
      fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
      lineHeight: "1.4",
      fontSize: (listener: Listener) => themeSize(listener, "decrease-1"),
      color: (listener: Listener) => themeColor(listener, "shift-6"),
    } as StyleObject,
  } as DomphyElement<"div">;

  const spotlightBlob: DomphyElement<"div"> = {
    div: null,
    ariaHidden: "true",
    // A decorative blend-mode blob with no text of its own — exempt from
    // the missing-color contract, matching this package's other purely
    // decorative canvas/glow elements (e.g. `particles.ts`).
    _doctorDisable: "missing-color",
    _onMount: (node: ElementNode) => {
      spotlightElement = node.domElement as HTMLElement;
    },
    _onRemove: () => {
      spotlightElement = null;
    },
    style: {
      position: "absolute",
      top: "var(--evervault-y, 50%)",
      left: "var(--evervault-x, 50%)",
      width: `${spotlightSize}px`,
      height: `${spotlightSize}px`,
      transform: "translate(-50%, -50%)",
      borderRadius: "50%",
      opacity: 0,
      transition: "opacity 200ms ease",
      mixBlendMode: "color",
      pointerEvents: "none",
      background: (listener: Listener) =>
        `radial-gradient(circle, ${themeColor(listener, "shift-9", "primary")} 0%, ${themeColor(listener, "shift-9", "info")} 45%, ${themeColor(listener, "shift-9", "secondary")} 75%, transparent 100%)`,
      filter: `blur(${themeSpacing(4)})`,
    } as StyleObject,
  } as DomphyElement<"div">;

  const isolatedLayer: DomphyElement<"div"> = {
    div: [characterGrid, spotlightBlob],
    style: { position: "absolute", inset: 0, isolation: "isolate", overflow: "hidden" } as StyleObject,
  } as DomphyElement<"div">;

  const titleLayer: DomphyElement<"div"> = {
    div: [{ h3: title, $: [heading()] } as DomphyElement],
    style: {
      position: "relative",
      zIndex: 1,
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      height: "100%",
    } as StyleObject,
  } as DomphyElement<"div">;

  return {
    div: [
      isolatedLayer,
      titleLayer,
      cornerPlus({ top: themeSpacing(2), left: themeSpacing(2) }),
      cornerPlus({ top: themeSpacing(2), right: themeSpacing(2) }),
      cornerPlus({ bottom: themeSpacing(2), left: themeSpacing(2) }),
      cornerPlus({ bottom: themeSpacing(2), right: themeSpacing(2) }),
    ],
    dataTone: "shift-16",
    style: {
      position: "relative",
      overflow: "hidden",
      borderRadius: themeSpacing(4),
      minHeight: themeSpacing(56),
      backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
      color: (listener: Listener) => themeColor(listener, "shift-9"),
      outline: (listener: Listener) => `1px solid ${themeColor(listener, "shift-3")}`,
      outlineOffset: "-1px",
      ...(props.style ?? {}),
    } as StyleObject,
    _onMount: (node: ElementNode) => {
      if (typeof window === "undefined") return;
      const cardElement = node.domElement as HTMLElement | null;
      if (!cardElement) return;

      const handlePointerMove = (event: MouseEvent) => {
        const rect = cardElement.getBoundingClientRect();
        cardElement.style.setProperty("--evervault-x", `${event.clientX - rect.left}px`);
        cardElement.style.setProperty("--evervault-y", `${event.clientY - rect.top}px`);
        if (spotlightElement) spotlightElement.style.opacity = "1";
      };
      const handlePointerLeave = () => {
        if (spotlightElement) spotlightElement.style.opacity = "0";
      };
      cardElement.addEventListener("mousemove", handlePointerMove);
      cardElement.addEventListener("mouseleave", handlePointerLeave);

      const shuffleCount = Math.max(1, Math.round(totalCells * shuffleFraction));
      const shuffleIntervalId = setInterval(() => {
        for (let tick = 0; tick < shuffleCount; tick += 1) {
          const index = Math.floor(Math.random() * totalCells);
          const element = characterElementRefs[index];
          if (element) element.textContent = randomCharacter(characterPool);
        }
      }, shuffleIntervalMs);

      node.addHook("Remove", () => {
        cardElement.removeEventListener("mousemove", handlePointerMove);
        cardElement.removeEventListener("mouseleave", handlePointerLeave);
        clearInterval(shuffleIntervalId);
      });
    },
  } as DomphyElement<"div">;
}

export { evervaultCard };

← Back to Aceternity UI catalog