Skip to content
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.

Props

PropTypeDescription
squareSizenumberSide length of each square, in canvas px. Defaults to 4.
gridGapnumberGap between squares, in canvas px. Defaults to 6.
flickerChancenumberProbability factor driving how often a cell rerolls its opacity (higher = more frequent flicker). Defaults to 0.3.
colorThemeColorTheme color family for the squares. Defaults to "neutral".
widthnumberFixed canvas width, in px. Omit to fill the parent container's measured width.
heightnumberFixed canvas height, in px. Omit to fill the parent container's measured height.
maxOpacitynumberCeiling for each cell's randomized opacity. Defaults to 0.3.
childrenDomphyElement | DomphyElement[]Foreground content layered above the grid. Defaults to a small demo heading.
styleStyleObjectPassthrough style merged onto the outer container.
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 { behavior } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeColorToken,
  themeSpacing,
} from "@domphy/theme";
import { heading, paragraph } from "@domphy/ui";

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;
}

const FLICKERING_GRID_BEHAVIOR_KEY = "magicui-flickering-grid";

interface FlickeringGridBehaviorProps {
  squareSize: number;
  gridGap: number;
  flickerChance: number;
  color: ThemeColor;
  fixedWidth?: number;
  fixedHeight?: number;
  maxOpacity: number;
}

function attachFlickeringGrid(
  node: ElementNode,
  initialProps: FlickeringGridBehaviorProps,
) {
  let props = initialProps;
  const canvasElement = node.domElement as HTMLCanvasElement | null;
  const host = canvasElement?.parentElement ?? null;
  if (!canvasElement || !host || typeof window === "undefined") {
    return { update() {}, destroy() {} };
  }
  const canvas: HTMLCanvasElement = canvasElement;
  const containerElement: HTMLElement = host;

  const drawingContext = canvas.getContext("2d");
  if (!drawingContext) return { update() {}, destroy() {} };
  const context: CanvasRenderingContext2D = drawingContext;

  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;

  const fillColor = (() => {
    try {
      return themeColorToken(node, "shift-6", props.color);
    } catch {
      return "#888888";
    }
  })();

  function resizeCanvas(): void {
    const rect = containerElement.getBoundingClientRect();
    cssWidth = props.fixedWidth ?? rect.width;
    cssHeight = props.fixedHeight ?? rect.height;
    canvas.width = Math.max(1, Math.floor(cssWidth * devicePixelRatio));
    canvas.height = Math.max(1, Math.floor(cssHeight * devicePixelRatio));
    context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);

    const dimensions = computeGridDimensions(
      cssWidth,
      cssHeight,
      props.squareSize,
      props.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() * props.maxOpacity;
    }
    opacities = nextOpacities;
  }

  function drawGrid(): void {
    context.clearRect(0, 0, cssWidth, cssHeight);
    context.fillStyle = fillColor;
    const cellSpan = props.squareSize + props.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,
          props.squareSize,
          props.squareSize,
        );
      }
    }
    context.globalAlpha = 1;
  }

  function tick(time: number): void {
    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() < props.flickerChance * deltaSeconds) {
        opacities[index] = Math.random() * props.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 {
    startLoop();
  }

  return {
    update(next: FlickeringGridBehaviorProps) {
      props = next;
    },
    destroy() {
      stopLoop();
      resizeObserver?.disconnect();
      intersectionObserver?.disconnect();
    },
  };
}

function computeGridDimensions(
  containerWidth: number,
  containerHeight: number,
  squareSize: number,
  gridGap: number,
): { columns: number; rows: number } {
  const cellSpan = squareSize + gridGap;
  return {
    columns: Math.max(1, Math.ceil(containerWidth / cellSpan)),
    rows: Math.max(1, Math.ceil(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",
    },
    ...behavior<FlickeringGridBehaviorProps>(
      FLICKERING_GRID_BEHAVIOR_KEY,
      attachFlickeringGrid,
      {
        squareSize,
        gridGap,
        flickerChance,
        color,
        fixedWidth,
        fixedHeight,
        maxOpacity,
      },
    ),
  } 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