Backgrounds

Flickering Grid

A deterministic canvas square grid with softly changing opacity, responsive density controls and a reduced-motion still frame.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

const MAX_DPR = 2
const DEFAULT_SEED = 0x7f4a7c15

function finiteNumber(value: number, fallback: number) {
  return Number.isFinite(value) ? value : fallback
}

function finiteCssSize(
  value: React.CSSProperties["width"] | React.CSSProperties["height"],

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/flickering-grid.json

Prompt

Build a React + TypeScript + Tailwind "FlickeringGrid" component — a
deterministic canvas square matrix used as a decorative content background.
Its only code dependency is a cn() class merger.

Contract
- Export FlickeringGridProps extending div props (omit the legacy color
  attribute) with squareSize?: number (default 4), gap?: number (default 6),
  flickerChance?: number (default 0.8 changes per square per second),
  maxOpacity?: number (default .28), color?: string (default "currentColor"),
  width?/height? as CSS sizes, frameRate?: number (default 30), seed?: number,
  children and className.
- Reject NaN/Infinity with Number.isFinite fallbacks, then clamp squareSize to
  [1,64], gap to [0,128], flickerChance to [0,60], maxOpacity to [0,1],
  frameRate to an integer in [1,60], and seed to a finite integer. Sanitize
  numeric width/height the same way. Spread root props, merge className through
  cn(), and let explicit width/height override the corresponding consumer style.

Behavior
- Root is relative/isolate/overflow-hidden. A size-full canvas is absolute,
  pointer-events-none and aria-hidden; optional children render in a relative
  z-10 foreground layer, so decoration can never capture clicks.
- ResizeObserver measures the canvas. Set its backing store to CSS size ×
  min(devicePixelRatio, 2), then call context.setTransform(dpr,0,0,dpr,0,0)
  because changing width/height resets the context. Draw in CSS pixels.
- Build ceil(width / (squareSize + gap)) columns and equivalent rows. Initial
  opacity values and all later changes come from a seeded 32-bit LCG, never
  Math.random(), so screenshots remain reproducible.
- Run one requestAnimationFrame loop but throttle paint to frameRate. Convert
  flickerChance from changes-per-second into a delta-time probability with
  1 - exp(-chance * dt); cap dt after pauses before updating each cell.
- Listen for visibilitychange: cancel rAF while document.visibilityState is
  hidden, then repaint and restart with a reset time base on return. Disconnect
  ResizeObserver/MutationObserver, cancel rAF and remove the listener on
  unmount.
- Read getComputedStyle(canvas).color as the ink. One MutationObserver watches
  class/style/data-theme on every node in the canvas-to-documentElement
  ancestor chain. Changing the color prop, a text-* class, root inline color,
  a local preview wrapper's CSS variables or the global theme therefore
  re-reads ink and redraws immediately even when reduced motion has paused rAF.
- Subscribe to prefers-reduced-motion with useSyncExternalStore and a server
  snapshot of false. Under reduce, build and paint exactly one deterministic
  frame but never start rAF.

Rendering & styling
- Semantic color only: root defaults to text-muted-foreground, the canvas
  inherits currentColor, and callers may pass color="var(--primary)" or
  another theme token. Opacity lives in context.globalAlpha. No hex/rgb/oklch
  color literals and no hand parsing.
- The root has no opinionated fixed height beyond a small safety minimum;
  className or width/height props own the real surface dimensions. Children
  explicitly use text-foreground above the muted decorative ink.
- Canvas is pure decoration (aria-hidden) while child content remains normal,
  selectable DOM.

Customization levers
- Density: squareSize and gap are independent. Smaller gap packs more cells;
  larger squareSize makes each cell visually louder.
- Cadence: flickerChance controls how often cells change, while frameRate
  controls paint cost. A calm hero can keep 30fps and lower chance.
- Contrast: maxOpacity is the ceiling; keep it low behind copy. Recolor with
  a semantic token through color or a text-* class on the root.
- Footprint: className is the normal responsive sizing path; fixed width and
  height are available for embedded widgets or deterministic screenshots.
- Reproducibility: change seed to create another stable field; do not replace
  the LCG with Math.random() inside render or the loop.

Concepts

  • Deterministic opacity field — a fixed-seed LCG drives both initial alphas and later changes, making the visual repeatable for screenshots and avoiding render-time randomness.
  • DPR-capped backing store — the canvas uses device-pixel resolution for crisp squares but caps DPR at two, preventing high-density phones from multiplying fill cost with little visible benefit.
  • Time-based flicker rate — change probability derives from elapsed seconds rather than frame count, so lowering frameRate reduces work without making the grid look artificially slow.
  • Visibility-paused animation — a hidden document cancels the frame loop completely and resets its clock before resuming, avoiding background CPU and a burst of stale updates.
  • currentColor ink — the canvas reads its computed text color and applies alpha through the drawing context; semantic text tokens therefore theme the grid and dark mode without color parsing.
  • Reduced-motion static frame — motion reduction keeps one complete seeded grid visible while never scheduling the animation loop, preserving decoration without flicker.

On This Page