Skip to main content

Cell Wash Grid

An interactive grid background where squares raise in a tinted wash and dissolve back out: each cell schedules its own random light-up loop (delay, hold, fade, reschedule) and hovering a cell paints it instantly. Lit cells snap on with a 0ms transition and fade over a configurable duration, all in a single accent hue stepped across five lightness stops.

Micro InteractionReactTailwind CSS
CSSTailwind

Manual

Create a file and paste the following code into it.

cell-wash-grid.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"use client";

import { useCallback, useEffect, useState } from "react";
import { cn } from "@/lib/cn";

/* ------------------------------------------------------------------ */
/*  CellWashGrid                                                      */
/* ------------------------------------------------------------------ */

/** One hue, five steps — status is a wash of a single accent, not a rainbow. */
const CELL_COLORS = [
  "bg-[oklch(0.55_0.2_255/0.55)]",
  "bg-[oklch(0.62_0.19_252/0.5)]",
  "bg-[oklch(0.7_0.16_248/0.45)]",
  "bg-[oklch(0.78_0.12_244/0.4)]",
  "bg-[oklch(0.86_0.08_240/0.35)]",
];

function randomColor() {
  return CELL_COLORS[Math.floor(Math.random() * CELL_COLORS.length)];
}

interface CellWashGridProps {
  /** Cells per side (total = gridSize squared) [Optional, default: 10] */
  gridSize?: number;
  /** Random light-up window start in ms [Optional, default: 0] */
  minDelay?: number;
  /** Random light-up window end in ms [Optional, default: 5500] */
  maxDelay?: number;
  /** How long a lit cell stays on, min ms [Optional, default: 500] */
  minDuration?: number;
  /** How long a lit cell stays on, max ms [Optional, default: 1500] */
  maxDuration?: number;
  /** Fade-out transition in ms [Optional, default: 1000] */
  fadeDuration?: number;
  /** Ambient random lighting on mount [Optional, default: true] */
  autoAnimate?: boolean;
  className?: string;
}

/**
 * An interactive grid where squares raise in a tinted wash and fade back out:
 * each cell schedules its own random light-up loop (delay, hold, fade,
 * reschedule), and hovering a cell lights it instantly. Lit cells snap on
 * (0ms) and dissolve over fadeDuration when released.
 * @param {number} gridSize - Cells per side [Optional, default: 10]
 * @param {boolean} autoAnimate - Ambient loop on mount [Optional, default: true]
 */
export function CellWashGrid({
  gridSize = 10,
  minDelay = 0,
  maxDelay = 5500,
  minDuration = 500,
  maxDuration = 1500,
  fadeDuration = 1000,
  autoAnimate = true,
  className,
}: CellWashGridProps) {
  const total = gridSize * gridSize;
  const [hovered, setHovered] = useState<number | null>(null);
  const [colors, setColors] = useState<Record<number, string>>({});
  const [lit, setLit] = useState<ReadonlySet<number>>(new Set());

  const light = useCallback((index: number) => {
    setHovered(index);
    setColors((prev) => ({ ...prev, [index]: randomColor() }));
  }, []);

  useEffect(() => {
    if (!autoAnimate) {
      setLit(new Set());
      return;
    }
    const timers: ReturnType<typeof setTimeout>[] = [];
    const schedule = (index: number) => {
      const timer = setTimeout(() => {
        setLit((prev) => new Set([...prev, index]));
        setColors((prev) => ({ ...prev, [index]: randomColor() }));
        const off = setTimeout(() => {
          setLit((prev) => {
            const next = new Set(prev);
            next.delete(index);
            return next;
          });
          schedule(index);
        }, Math.floor(Math.random() * (maxDuration - minDuration)) + minDuration);
        timers.push(off);
      }, Math.floor(Math.random() * (maxDelay - minDelay)) + minDelay);
      timers.push(timer);
    };
    for (let i = 0; i < total; i++) schedule(i);
    return () => timers.forEach(clearTimeout);
  }, [autoAnimate, total, minDelay, maxDelay, minDuration, maxDuration]);

  return (
    <div
      className={cn("grid h-full w-full", className)}
      style={{ gridTemplateColumns: `repeat(${gridSize}, 1fr)` }}
    >
      {Array.from({ length: total }, (_, index) => {
        const isLit = hovered === index || lit.has(index);
        const color = colors[index];
        return (
          <div
            key={index}
            onMouseEnter={() => light(index)}
            onMouseLeave={() => setHovered(null)}
            style={{ transition: `background-color ${isLit ? "0ms" : `${fadeDuration}ms`}` }}
            className={cn(
              "aspect-square border-l border-t border-zinc-200/70 dark:border-zinc-800/70",
              isLit && color ? color : "bg-transparent",
            )}
          />
        );
      })}
    </div>
  );
}

Update the import paths to match your project setup.

Similar components

Tilt Pointer

Halftone Portrait

Edge Veil Blur

Spotlight Follow

Install via CLI

Resource details

PublishedJuly 17, 2026
CategoryMicro Interaction
ReactTailwind CSS