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…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/flickering-grid.jsonPrompt
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.