Grid Background with Fading Random Squares

A white hero with a fine indigo grid behind it. Random squares on the grid fill with colour, fade out again and then reappear somewhere else, so the pattern never repeats. The grid is tilted a few degrees and masked so it is strongest in the middle and gone at the edges, and a soft white patch keeps the headline easy to read. The hero has a badge, a headline, two buttons and three stat cards that stack on phones.

Code

// Adapted from Magic UI (https://magicui.design/docs/components/animated-grid-pattern)
// Copyright (c) Magic UI. MIT License: https://we.inc/components/licenses
import { useCallback, useEffect, useId, useRef, useState } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { ArrowRight } from "lucide-react";

const stats = [
  { value: "14 days", label: "from signed quote to first panel" },
  { value: "25 years", label: "output warranty on every array" },
  { value: "3,100", label: "roofs fitted across the county" },
];

type Square = { id: number; pos: [number, number]; iteration: number };

/**
 * A ruled grid drawn with an svg pattern. Random cells fade in, fade out
 * and then jump to a new place.
 */
function FadingGrid({ cell = 44, count = 44, maxOpacity = 0.75, duration = 2.6, className = "" }: { cell?: number; count?: number; maxOpacity?: number; duration?: number; className?: string }) {
  const id = useId();
  const reduce = useReducedMotion();
  const ref = useRef<SVGSVGElement | null>(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  const [squares, setSquares] = useState<Square[]>([]);

  const randomPos = useCallback((): [number, number] => [Math.floor((Math.random() * size.w) / cell), Math.floor((Math.random() * size.h) / cell)], [size.w, size.h, cell]);

  // Fill the grid once its size is known, and again if it is resized.
  useEffect(() => {
    if (!size.w || !size.h) return;
    setSquares(Array.from({ length: count }, (_, i) => ({ id: i, pos: randomPos(), iteration: 0 })));
  }, [size.w, size.h, count, randomPos]);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const ro = new ResizeObserver(([entry]) => {
      const { width, height } = entry.contentRect;
      setSize((s) => (s.w === width && s.h === height ? s : { w: width, h: height }));
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // When a square has faded out, move it somewhere else and play it again.
  const move = useCallback(
    (squareId: number) => {
      setSquares((list) => list.map((sq) => (sq.id === squareId ? { ...sq, pos: randomPos(), iteration: sq.iteration + 1 } : sq)));
    },
    [randomPos],
  );

  return (
    <svg ref={ref} aria-hidden="true" className={`pointer-events-none absolute inset-0 h-full w-full ${className}`}>
      <defs>
        <pattern id={id} width={cell} height={cell} patternUnits="userSpaceOnUse" x={-1} y={-1}>
          <path d={`M.5 ${cell}V.5H${cell}`} fill="none" stroke="currentColor" strokeOpacity={0.4} />
        </pattern>
      </defs>
      <rect width="100%" height="100%" fill={`url(#${id})`} />
      <svg x={-1} y={-1} className="overflow-visible">
        {squares.map(({ pos: [x, y], id: squareId, iteration }, index) =>
          reduce ? (
            <rect key={squareId} width={cell - 1} height={cell - 1} x={x * cell + 1} y={y * cell + 1} fill="currentColor" opacity={maxOpacity * (0.3 + ((index * 7) % 10) / 14)} />
          ) : (
            <motion.rect
              key={`${squareId}-${iteration}`}
              // The first round starts part of the way in, so the grid is never empty.
              initial={{ opacity: iteration === 0 ? maxOpacity * ((index * 7) % 10) * 0.08 : 0 }}
              animate={{ opacity: maxOpacity }}
              transition={{ duration: duration * (0.7 + ((index * 3) % 5) * 0.15), repeat: 1, repeatType: "reverse", delay: iteration === 0 ? index * 0.04 : 0, repeatDelay: 0.3, ease: "easeInOut" }}
              onAnimationComplete={() => move(squareId)}
              width={cell - 1}
              height={cell - 1}
              x={x * cell + 1}
              y={y * cell + 1}
              fill="currentColor"
            />
          ),
        )}
      </svg>
    </svg>
  );
}

export default function GridBackgroundWithFadingRandomSquares() {
  return (
    <section className="relative overflow-hidden bg-white px-5 py-20 sm:px-6 sm:py-28">
      {/* The mask keeps the grid strong in the middle and lets it fade toward the edges. */}
      <div aria-hidden="true" className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white_25%,transparent_78%)]">
        <FadingGrid className="-skew-y-6 scale-125 text-indigo-500" />
      </div>
      <span aria-hidden="true" className="pointer-events-none absolute left-1/2 top-1/2 h-56 w-[560px] max-w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-white blur-2xl" />

      <div className="relative mx-auto max-w-3xl text-center">
        <p className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-white px-3.5 py-1.5 text-sm font-medium text-indigo-700 shadow-sm">
          <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
          Spring installs are open
        </p>
        <h1 className="mt-7 text-4xl font-semibold tracking-tight text-slate-900 sm:text-6xl sm:leading-[1.05]">Your roof, earning its keep one panel at a time.</h1>
        <p className="mx-auto mt-6 max-w-xl text-lg leading-8 text-slate-600">Brightfield designs, fits and looks after home solar. You get a fixed quote from a satellite survey, and a live view of every panel once it is running.</p>
        <div className="mt-9 flex flex-wrap items-center justify-center gap-3">
          <a href="#" className="group inline-flex h-12 items-center gap-2 rounded-xl bg-indigo-600 px-6 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition hover:bg-indigo-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2">
            Get my roof surveyed
            <ArrowRight aria-hidden="true" className="h-4 w-4 transition-transform group-hover:translate-x-1" />
          </a>
          <a href="#" className="inline-flex h-12 items-center rounded-xl border border-slate-300 bg-white px-6 text-sm font-semibold text-slate-900 transition hover:border-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2">
            See what it saves
          </a>
        </div>

        <dl className="mt-16 grid gap-4 sm:grid-cols-3">
          {stats.map((s) => (
            <div key={s.label} className="rounded-2xl border border-slate-200 bg-white/90 p-5 text-left">
              <dt className="sr-only">{s.label}</dt>
              <dd className="text-2xl font-semibold tracking-tight text-slate-900">{s.value}</dd>
              <dd className="mt-1 text-sm text-slate-600">{s.label}</dd>
            </div>
          ))}
        </dl>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/grid-background-with-fading-random-squares.json

When to use it

How to customize it

Accessibility

Questions

How is the grid drawn?

An svg `pattern` holds one L-shaped path the size of a cell, and a full-size `rect` is filled with that pattern. The browser tiles it, so the grid costs almost nothing however large the section is.

How do the squares pick where to appear?

A `ResizeObserver` measures the svg. Each square gets a random column and row inside that size. When its fade in and out has finished, it is given a new position and a new key, which restarts its animation in the new cell.

Why are some squares already visible when the page loads?

On the first round each square starts at a different opacity and with a small delay. Without that, the grid would be empty for the first second and then every square would pulse together.

Will it slow the page down?

It animates the opacity of a few dozen svg rectangles, which is light. Keep `count` under about sixty; more than that adds work without making the pattern look better.

Can I use it on a dark background?

Yes. Set the section to a dark colour, use a light text colour on `FadingGrid` such as `text-sky-300`, and change the mask colours so the centre stays visible.

Adapted from Animated Grid Pattern in Magic UI, used under its open-source licence. Licences and credits

Related: Animated Grid Background with Travelling Light Beams, Dot Matrix Background with Ripples and Pointer Spotlight, Flickering Pixel Grid Canvas Background.

More animated backgrounds or all free UI components. Start for free to have a full site built around it.

More in Backgrounds

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap