Domphy

dotPattern

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

Implementation notes

Deliberately does NOT reuse gridPattern's zero-JS <pattern>-tiling technique: glow mode needs each visible dot to animate on its own independently randomized duration/delay, and a single <pattern> tile stamps identical content at every repeat, so no tile can carry per-instance randomness. Instead the dot grid is individual <circle> elements, managed imperatively -- on mount and on every ResizeObserver firing, the container is measured, the previous circle set is cleared, and a fresh grid (columns/rows = measured size / spacing) is appended via document.createElementNS, matching the spec's own research note ('recalculated from the container's measured pixel size divided by spacing'). This runs from the <g> layer's own _onMount (node.domElement is that <g> directly, already appended under its parent <svg> by the time Mount fires) rather than an ancestor querying for it, sidestepping the same Mount-fires-before-children ordering issue documented in animatedGridPattern. Non-glow dots are plain fill='currentColor' (color set once on the <svg> root); glow dots read from a shared <radialGradient> (bright core -> transparent) and each get randomized animation-duration (1.6-3.0s) / animation-delay (0-3s) on one shared scale+opacity 'there and back' @keyframes, so no two dots twinkle in sync. jsdom/non-layout runtimes (0x0 getBoundingClientRect) fall back to a fixed 320x200 default grid, exercised by this file's test.

Status: ported · Reference: Magic UI original

// Magic UI "Dot Pattern" — clean-room reimplementation.
//
// A background of evenly spaced small dots (dot-grid paper style), with an
// optional per-dot twinkling glow animation. Implemented purely from the
// block's public functional/visual spec — no upstream Magic UI source was
// viewed or copied.
//
// Unlike this package's `gridPattern` (a single tiled SVG `<pattern>`, which
// stamps identical content at every tile and so cannot give any one tile its
// own independent random timing), `glow` mode needs each visible dot to
// animate on its own randomized duration/delay. That rules out a `<pattern>`
// tile here, so the dot grid is built from individual `<circle>` elements
// instead, managed imperatively: on mount (and on every `ResizeObserver`
// firing), the container is measured, the previous circle set is cleared,
// and a fresh grid sized to the real pixel dimensions divided by spacing is
// appended directly via `document.createElementNS` — bypassing Domphy's
// declarative diffing for this one dynamic-count layer, the same way
// `particles`/`flickeringGrid` manage their canvas pixels imperatively
// rather than through the declarative tree. Non-glow dots are plain
// `fill="currentColor"` circles (no per-dot color at all); glow dots read
// from a shared `<radialGradient>` (bright core fading to transparent) and
// each get their own randomized `animation-duration`/`animation-delay` on
// one shared "there and back" scale+opacity `@keyframes`, so no two dots
// twinkle in sync.

import type { DomphyElement, ElementNode, 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 DotPatternProps {
  /** Horizontal spacing between dots, in px. Defaults to `16`. */
  width?: number;
  /** Vertical spacing between dots, in px. Defaults to `16`. */
  height?: number;
  /** Whole-pattern horizontal offset, in px. Defaults to `0`. */
  x?: number;
  /** Whole-pattern vertical offset, in px. Defaults to `0`. */
  y?: number;
  /** Per-dot horizontal center offset within its cell, in px. Defaults to `1`. */
  cx?: number;
  /** Per-dot vertical center offset within its cell, in px. Defaults to `1`. */
  cy?: number;
  /** Dot radius, in px. Defaults to `1`. */
  cr?: number;
  /** Turns on the animated glow (randomized per-dot pulse). Defaults to `false`. */
  glow?: boolean;
  /** Theme color family for the dots/glow core. Defaults to `"neutral"`. */
  color?: ThemeColor;
  /** Foreground content layered above the dot field. Defaults to a small demo heading. */
  children?: DomphyElement | DomphyElement[];
  /** Passthrough style merged onto the outer container. */
  style?: StyleObject;
}

let dotPatternInstanceCounter = 0;

const GLOW_PULSE_KEYFRAMES = {
  "0%,100%": { transform: "scale(0.85)", opacity: "0.6" },
  "50%": { transform: "scale(1.25)", opacity: "1" },
};

/**
 * A background of evenly spaced dots, optionally twinkling with an
 * independently-randomized glow per dot. Call with no arguments for a
 * working demo — a dark panel with a static dot grid behind a heading.
 */
function dotPattern(props: DotPatternProps = {}): DomphyElement<"div"> {
  const spacingWidth = Math.max(1, props.width ?? 16);
  const spacingHeight = Math.max(1, props.height ?? 16);
  const patternOffsetX = props.x ?? 0;
  const patternOffsetY = props.y ?? 0;
  const dotOffsetX = props.cx ?? 1;
  const dotOffsetY = props.cy ?? 1;
  const dotRadius = props.cr ?? 1;
  const glow = props.glow ?? false;
  const color = props.color ?? "neutral";

  const instanceId = ++dotPatternInstanceCounter;
  const glowGradientId = `domphy-dot-pattern-glow-${instanceId}`;
  const glowAnimationName = `dot-pattern-glow-pulse-${hashString(
    JSON.stringify({ instanceId, GLOW_PULSE_KEYFRAMES }),
  )}`;

  const contentChildren: DomphyElement[] = props.children
    ? Array.isArray(props.children)
      ? props.children
      : [props.children]
    : [
        { h2: "Dot Pattern", $: [heading()] } as DomphyElement,
        {
          p: glow
            ? "A dot grid where every dot twinkles on its own random cadence."
            : "A static dot-grid-paper background.",
          $: [paragraph()],
        } as DomphyElement,
      ];

  const glowDefs: DomphyElement[] = glow
    ? [
        {
          radialGradient: [
            {
              stop: null,
              offset: "0%",
              // Decorative gradient stop, no text of its own.
              _doctorDisable: "missing-color",
              style: { stopColor: (listener: Listener) => themeColor(listener, "shift-11", color) } as StyleObject,
            } as DomphyElement,
            {
              stop: null,
              offset: "100%",
              _doctorDisable: "missing-color",
              style: {
                stopColor: (listener: Listener) => themeColor(listener, "shift-11", color),
                stopOpacity: "0",
              } as StyleObject,
            } as DomphyElement,
          ],
          id: glowGradientId,
        } as DomphyElement,
      ]
    : [];

  const dotLayer: DomphyElement = {
    g: [],
    dataDotPatternLayer: "true",
    // Mount hooks fire top-down, right after THIS node's own DOM element is
    // created and appended into its parent (see `ElementNode.render`'s
    // recursion order) — so `node.domElement` here is always a real `<g>`
    // already attached under the outer `<svg>`, with no need to query for
    // it from an ancestor (which would fire too early, before this `<g>`
    // exists at all).
    _onMount: (node: ElementNode) => {
      const groupElement = node.domElement as SVGGElement | null;
      const svgElement = groupElement?.ownerSVGElement ?? null;
      if (!groupElement || !svgElement || typeof window === "undefined") return;
      const svgNamespace = "http://www.w3.org/2000/svg";

      let dotCircles: SVGCircleElement[] = [];

      function buildDot(pointX: number, pointY: number): SVGCircleElement {
        const circle = document.createElementNS(svgNamespace, "circle");
        circle.setAttribute("cx", String(pointX));
        circle.setAttribute("cy", String(pointY));
        circle.setAttribute("r", String(dotRadius));
        if (glow) {
          circle.setAttribute("fill", `url(#${glowGradientId})`);
          const durationSeconds = 1.6 + Math.random() * 1.4;
          const delaySeconds = Math.random() * 3;
          circle.style.transformBox = "fill-box";
          circle.style.transformOrigin = "center";
          circle.style.animation = `${glowAnimationName} ${durationSeconds}s ease-in-out ${delaySeconds}s infinite`;
        } else {
          circle.setAttribute("fill", "currentColor");
        }
        return circle;
      }

      function reflow(): void {
        const rect = svgElement!.getBoundingClientRect();
        // jsdom/non-layout runtimes measure 0×0 — fall back to a modest
        // default grid so the component still renders something structural.
        const measuredWidth = rect.width || 320;
        const measuredHeight = rect.height || 200;
        const columns = Math.max(1, Math.ceil(measuredWidth / spacingWidth) + 1);
        const rows = Math.max(1, Math.ceil(measuredHeight / spacingHeight) + 1);

        // The grid is small/cheap enough (dozens to a few hundred cells) that
        // a full rebuild on every resize is simpler and safer than diffing
        // individual dot survival across a changed column/row count.
        for (const circle of dotCircles) circle.remove();
        dotCircles = [];
        for (let row = 0; row < rows; row += 1) {
          for (let column = 0; column < columns; column += 1) {
            const pointX = patternOffsetX + dotOffsetX + column * spacingWidth;
            const pointY = patternOffsetY + dotOffsetY + row * spacingHeight;
            const circle = buildDot(pointX, pointY);
            groupElement!.appendChild(circle);
            dotCircles.push(circle);
          }
        }
      }

      reflow();

      let resizeObserver: ResizeObserver | null = null;
      if (typeof ResizeObserver !== "undefined") {
        resizeObserver = new ResizeObserver(() => reflow());
        resizeObserver.observe(svgElement);
      }

      node.addHook("Remove", () => {
        resizeObserver?.disconnect();
        for (const circle of dotCircles) circle.remove();
        dotCircles = [];
      });
    },
  } as DomphyElement;

  const gridSvg: DomphyElement = {
    svg: [...(glowDefs.length ? [{ defs: glowDefs } as DomphyElement] : []), dotLayer],
    ariaHidden: "true",
    style: {
      position: "absolute",
      inset: 0,
      width: "100%",
      height: "100%",
      pointerEvents: "none",
      color: (listener: Listener) => themeColor(listener, "shift-6", color),
    } as StyleObject,
  } as DomphyElement;

  return {
    div: [
      gridSvg,
      {
        div: contentChildren,
        style: { position: "relative", zIndex: 1 },
      } as DomphyElement,
    ],
    dataTone: "shift-15",
    style: {
      position: "relative",
      overflow: "hidden",
      borderRadius: themeSpacing(4),
      padding: themeSpacing(8),
      minHeight: themeSpacing(64),
      backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
      color: (listener: Listener) => themeColor(listener, "shift-9"),
      ...(props.style ?? {}),
    } as StyleObject,
  };
}

export { dotPattern };

← Back to Magic UI catalog