registry
@automindzsheets 01–77shadcn 4.x
SHEET 54

Flickering Grid

component / background

A field of squares flickering behind a hero on a canvas, drawn in the container's own computed colour, capped at 1.5Hz and stopped when it scrolls off screen.

type
component
files
1
registry deps
none
npm deps
none

installs

  • components/automindz/flickering-grid.tsx
  • Canvas, because CSS has no randomness — the family's only client boundary
  • Colour is the element's computed color, never a prop and never a hex
  • flickerChance is a rate in Hz, floored at 1.5 — half the WCAG threshold
  • The loop stops off screen; the first frame back is clamped, so no pop
  • Server render, first frame and reduced motion are one even field

No stylesheet, no config file and no required prop: <FlickeringGrid /> is the whole integration. **The caller wires one thing: the parent.** Like Backdrop, GridBeams, GlyphMatrix and InteractiveGrid this renders absolute inset-0 -z-10, so its container needs relative isolate — without a stacking context the negative z-index escapes upward and the field paints behind an ancestor's background. <section className="relative isolate overflow-hidden py-24"><FlickeringGrid /><h1>…</h1></section>. **This is the one item in the background family with a client boundary, and canvas is the reason.** The others are deterministic — every cell's phase is a hash of its position, so CSS holds the whole field — but a flicker is thousands of squares each re-randomising on their own schedule, and CSS has no random function; the nearest it gets is a hashed animation-delay, which is a loop and reads as a repeating pattern within about ten seconds. <canvas> and the two observers are platform APIs, so dependencies is still empty, and the boundary is one file that takes no callbacks — a Server Component can render <FlickeringGrid /> without becoming a client component. **There is no colour prop and no hex.** The squares are drawn in the container's *computed* color, so text-foreground is the default and any colour utility is the whole API; the colour is re-read when the theme changes, watched two ways because either alone misses half the cases — a MutationObserver over every element from the layer up to <html> for the class every theme library toggles, wherever it lands, and a prefers-color-scheme listener for the system flip that leaves no class behind. The two schemes are not the same alpha, for the reason Backdrop's table exists, so the layer carries a per-theme multiplier: --automindz-flicker-alpha (1) and --automindz-flicker-alpha-dark (0.7). **A server render is not a blank hole.** A canvas is empty until it is measured, so the layer ships its resting field in CSS — a background-image of vertical bars cut into squares by a mask-image of horizontal ones, same pitch, same currentColor, at maxOpacity × 0.5, which is the mean opacity of the running field and therefore the same density rather than a placeholder. The canvas hides it on its first painted frame and starts from that identical even field, so the flicker emerges over the first seconds instead of cutting in, and nothing shifts in layout at any point because the layer is out of flow. prefers-reduced-motion: reduce draws that same even field once and never starts the loop; toggling the setting live settles or restarts it. **It stops when it is off screen** — an IntersectionObserver cancels the frame loop and resumes it on return, because an animation running behind the fold on every page is a battery bug; the field is not reset on the way out, so there is no pop either way. **flickerChance is a rate in Hz, not a per-frame probability**: it is how many times a second a given square changes, which makes the WCAG 2.3.1 flash threshold directly comparable, and it is clamped in arithmetic to 1.5Hz — half of it — so flickerChance={6} runs at 1.5. The frame delta is clamped too, at 1/20s: IntersectionObserver does not fire for a tab switch, so without the clamp the first frame back from a background tab is worth thirty seconds and every square in the field re-rolls at once. **The grid is capped in arithmetic**: the pitch floors at 3px and the square count at 120 000, and the cap grows the pitch and the square together so a capped field is the same design at a coarser scale. A zero-size container renders nothing rather than throwing; there is no loading state and none is meaningful — the layer takes no data, and the pre-paint picture is the finished picture. Contrast is the same caveat Backdrop carries: text-foreground clears 4.5:1 over the field at every default, text-muted-foreground does not, so muted metadata belongs on a card or wants a lower maxOpacity. Props: squareSize (4), gridGap (6), flickerChance (0.3), maxOpacity (0.3), and optional fixed width / height for a layer that should not fill its parent.

Variations

one state per section

01

Behind a hero

The defaults: 4px squares on a 10px pitch, each square re-rolling about once every three seconds. Watch it for ten seconds — nothing about it repeats, and the button is clickable, because the whole layer is pointer-events: none.

fluid

Every environment, one deploy log

Preview, staging and production read from the same manifest, so a rollback is one commit and not an afternoon. defaults

02

The resting field

flickerChance={0} holds the picture three other renders show: the server's, the canvas's first frame, and prefers-reduced-motion. It is an even field at half maxOpacity — the moving field's own mean — so the flicker emerges out of it rather than cutting to it.

fluid

Every environment, one deploy log

Preview, staging and production read from the same manifest, so a rollback is one commit and not an afternoon. flickerChance={0}

03

Square and gap

The two dials that change what the field is: a coarse mosaic, the default weave, and a fine grain. The pitch is floored at 3px and the square count capped at 120 000, so a squareSize of a fraction gets a legible field instead of a locked tab.

fluid
14 / 6
4 / 6
1 / 2
04

Rate, and the ceiling

flickerChance is a rate in Hz — how often one square changes — so 0.1 is a slow shimmer and 0.3 is the default. The third tile asks for 6Hz and gets 1.5, half the WCAG flash threshold; the cap is arithmetic, not a line in the docs.

fluid
0.1Hz
0.3Hz
asked 6Hz → 1.5Hz
05

Colour and weight

No colour prop: the squares are drawn in the container's computed color, so a utility class is the whole API and the item follows the consumer's theme in both schemes. maxOpacity is the ceiling a square can reach, and the layer carries a per-theme multiplier so dark is not a cage.

fluid
text-foreground
text-primary
maxOpacity={0.8}