Domphy

flickeringGrid

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

Implementation notes

Canvas 2D 'particle grid' loop matching the spec exactly: a flat Float32Array holds one opacity per cell, each rAF tick rerolls a subset via Math.random() < flickerChance * deltaSeconds, only hit cells change. Gated by IntersectionObserver (loop stops off-screen) and ResizeObserver (recomputes columns/rows + reallocates the opacity array on resize), with devicePixelRatio-scaled backing store via context.setTransform (not .scale, to avoid compounding on repeated resizes -- a latent issue present in this package's existing particles.ts that this file avoids). Fill color is resolved once via themeColorToken() into a concrete hex string at mount time (canvas 2D has no var() concept) and does not live-update on a later runtime theme swap -- same documented limitation as particles.ts elsewhere in this package. Demo defaults were adapted to Domphy's dark shift-15 demo-panel convention (used consistently by particles/meteors in this package) rather than literally reproducing the spec's 'near-black on white' default; the color prop is a Domphy ThemeColor family, not a literal color string.

Status: ported · Reference: Magic UI original

// Magic UI "Flickering Grid" — clean-room reimplementation.
//
// A canvas-rendered grid of small squares whose individual opacities flicker
// randomly and continuously, producing a TV-static / circuit-board noise
// texture — typically layered behind hero content as ambient decoration.
// Implemented purely from the block's public functional/visual spec — no
// upstream Magic UI source was viewed or copied.
//
// Canvas 2D "particle grid" loop: a flat typed array holds one opacity value
// per grid cell. Each `requestAnimationFrame` tick computes the elapsed time
// since the previous frame, then for every cell rolls `Math.random() <
// flickerChance * deltaSeconds` — a hit re-randomizes that cell's opacity
// between `0` and `maxOpacity`; a miss leaves it untouched. Because only a
// small fraction of cells reroll per frame, the grid twinkles organically
// rather than pulsing in unison. An `IntersectionObserver` pauses the loop
// entirely while the canvas is scrolled out of view, and a `ResizeObserver`
// recomputes the column/row count (and reallocates the opacity array) when
// the container is resized. The canvas backing store is scaled by
// `devicePixelRatio` (capped at 2) for crisp edges on high-DPI screens; the
// fill color is resolved once via `themeColorToken` (a concrete hex string —
// canvas 2D has no notion of the `var(--…)` references `themeColor()`
// returns) and does not re-resolve on a later runtime theme swap. See this
// component's `fidelityNotes` for that tradeoff.

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

export interface FlickeringGridProps {
  /** Side length of each square, in canvas px. Defaults to `4`. */
  squareSize?: number;
  /** Gap between squares, in canvas px. Defaults to `6`. */
  gridGap?: number;
  /** Probability factor driving how often a cell rerolls its opacity (higher = more frequent flicker). Defaults to `0.3`. */
  flickerChance?: number;
  /** Theme color family for the squares. Defaults to `"neutral"`. */
  color?: ThemeColor;
  /** Fixed canvas width, in px. Omit to fill the parent container's measured width. */
  width?: number;
  /** Fixed canvas height, in px. Omit to fill the parent container's measured height. */
  height?: number;
  /** Ceiling for each cell's randomized opacity. Defaults to `0.3`. */
  maxOpacity?: number;
  /** Foreground content layered above the grid. Defaults to a small demo heading. */
  children?: DomphyElement | DomphyElement[];
  /** Passthrough style merged onto the outer container. */
  style?: StyleObject;
}

function computeGridDimensions(
  containerWidth: number,
  containerHeight: number,
  squareSize: number,
  gridGap: number,
): { columns: number; rows: number } {
  const cellSpan = squareSize + gridGap;
  return {
    columns: Math.max(1, Math.floor(containerWidth / cellSpan)),
    rows: Math.max(1, Math.floor(containerHeight / cellSpan)),
  };
}

/**
 * A canvas-rendered grid of squares that flicker between random opacities,
 * gated to only animate while scrolled into view. Call with no arguments for
 * a working demo — a dark panel with a twinkling grid behind a heading.
 */
function flickeringGrid(props: FlickeringGridProps = {}): DomphyElement<"div"> {
  const squareSize = Math.max(1, props.squareSize ?? 4);
  const gridGap = Math.max(0, props.gridGap ?? 6);
  const flickerChance = props.flickerChance ?? 0.3;
  const color = props.color ?? "neutral";
  const fixedWidth = props.width;
  const fixedHeight = props.height;
  const maxOpacity = props.maxOpacity ?? 0.3;

  const contentChildren: DomphyElement[] = props.children
    ? Array.isArray(props.children)
      ? props.children
      : [props.children]
    : [
        { h2: "Flickering Grid", $: [heading()] } as DomphyElement,
        {
          p: "A canvas grid of squares twinkling between random opacities.",
          $: [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 particles.ts).
  const canvasElement = {
    canvas: null,
    ariaHidden: "true",
    // Decorative canvas with no text of its own — fill color is resolved
    // imperatively below (canvas 2D has no themeColor() var() concept).
    _doctorDisable: "missing-color",
    style: {
      position: "absolute",
      inset: 0,
      width: fixedWidth ? `${fixedWidth}px` : "100%",
      height: fixedHeight ? `${fixedHeight}px` : "100%",
      pointerEvents: "none",
    },
    _onMount: (node: ElementNode) => {
      const canvas = node.domElement as HTMLCanvasElement | null;
      const containerElement = canvas?.parentElement ?? null;
      if (!canvas || !containerElement || typeof window === "undefined") return;

      // Headless/test runtimes without a real 2D canvas backend resolve
      // `getContext` to `null` rather than throwing — bail before starting.
      const context = canvas.getContext("2d");
      if (!context) return;

      const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);
      let cssWidth = 0;
      let cssHeight = 0;
      let columns = 0;
      let rows = 0;
      let opacities = new Float32Array(0);
      let lastFrameTime = 0;
      let animationFrameId: number | null = null;
      let resizeObserver: ResizeObserver | null = null;
      let intersectionObserver: IntersectionObserver | null = null;

      // shift-6 (a dim, muted step) so the flicker reads as a faint texture
      // against the container's own dark shift-15 surface — the per-cell
      // random alpha (capped at `maxOpacity`) on top does the rest.
      const fillColor = (() => {
        try {
          return themeColorToken(node, "shift-6", color);
        } catch {
          return "#888888";
        }
      })();

      function resizeCanvas(): void {
        const rect = containerElement!.getBoundingClientRect();
        cssWidth = fixedWidth ?? rect.width;
        cssHeight = fixedHeight ?? rect.height;
        canvas!.width = Math.max(1, Math.floor(cssWidth * devicePixelRatio));
        canvas!.height = Math.max(1, Math.floor(cssHeight * devicePixelRatio));
        // `setTransform` (not `scale`) so repeated resizes never compound the
        // device-pixel-ratio scale factor onto itself.
        context!.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);

        const dimensions = computeGridDimensions(cssWidth, cssHeight, squareSize, gridGap);
        columns = dimensions.columns;
        rows = dimensions.rows;
        const nextOpacities = new Float32Array(Math.max(1, columns * rows));
        for (let index = 0; index < nextOpacities.length; index += 1) {
          nextOpacities[index] = Math.random() * maxOpacity;
        }
        opacities = nextOpacities;
      }

      function drawGrid(): void {
        context!.clearRect(0, 0, cssWidth, cssHeight);
        context!.fillStyle = fillColor;
        const cellSpan = squareSize + gridGap;
        for (let row = 0; row < rows; row += 1) {
          for (let column = 0; column < columns; column += 1) {
            const index = row * columns + column;
            context!.globalAlpha = opacities[index] ?? 0;
            context!.fillRect(column * cellSpan, row * cellSpan, squareSize, squareSize);
          }
        }
        context!.globalAlpha = 1;
      }

      function tick(time: number): void {
        // Belt-and-suspenders: bail without rescheduling once the canvas is
        // no longer in the document, even if the IntersectionObserver above
        // never fires (e.g. unsupported in a test runtime, or the framework's
        // own "Remove" hook didn't run because of a raw DOM wipe).
        if (!canvas!.isConnected) {
          stopLoop();
          return;
        }
        const deltaSeconds = Math.min((time - lastFrameTime) / 1000, 0.2);
        lastFrameTime = time;
        for (let index = 0; index < opacities.length; index += 1) {
          if (Math.random() < flickerChance * deltaSeconds) {
            opacities[index] = Math.random() * maxOpacity;
          }
        }
        drawGrid();
        animationFrameId = window.requestAnimationFrame(tick);
      }

      function startLoop(): void {
        if (animationFrameId !== null) return;
        lastFrameTime = performance.now();
        animationFrameId = window.requestAnimationFrame(tick);
      }
      function stopLoop(): void {
        if (animationFrameId === null) return;
        window.cancelAnimationFrame(animationFrameId);
        animationFrameId = null;
      }

      resizeCanvas();
      drawGrid();

      if (typeof ResizeObserver !== "undefined") {
        resizeObserver = new ResizeObserver(() => {
          resizeCanvas();
          drawGrid();
        });
        resizeObserver.observe(containerElement);
      }

      if (typeof IntersectionObserver === "function") {
        intersectionObserver = new IntersectionObserver((entries) => {
          for (const entry of entries) {
            if (entry.isIntersecting) startLoop();
            else stopLoop();
          }
        });
        intersectionObserver.observe(containerElement);
      } else {
        // No IntersectionObserver support — fail open and animate always.
        startLoop();
      }

      node.addHook("Remove", () => {
        stopLoop();
        resizeObserver?.disconnect();
        intersectionObserver?.disconnect();
      });
    },
  } as DomphyElement<"canvas">;

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

export { flickeringGrid };

← Back to Magic UI catalog