A field of tiny squares that flicker softly, drawn on a canvas so it stays light.
Signal detected
Customize
Usage
import { FlickerGrid } from "@/components/motionkit/flicker-grid";
<FlickerGrid className="h-96 rounded-3xl">
<h1>Your hero</h1>
</FlickerGrid>Motion
The exact curves this component runs, plotted with Motion's own maths.
Flicker
Runs on its own
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/flicker-grid.tsx.
"use client";
import { useEffect, useRef, type ReactNode } from "react";
import { useReducedMotion } from "motion/react";
type FlickerGridProps = {
children?: ReactNode;
/** Square size in pixels. */
size?: number;
/** Space between squares in pixels. */
gap?: number;
/** 0–1. Chance each square changes brightness per second. */
chance?: number;
/** 0–1. Brightest a square gets. */
maxOpacity?: number;
/** Square colour. */
color?: string;
className?: string;
};
export function FlickerGrid({
children,
size = 4,
gap = 6,
chance = 0.3,
maxOpacity = 0.35,
color = "#2E9098",
className = "",
}: FlickerGridProps) {
const wrapperRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const reduceMotion = useReducedMotion();
// thousands of squares are drawn on a canvas, which stays cheap where DOM nodes would not
useEffect(() => {
const wrapper = wrapperRef.current;
const canvas = canvasRef.current;
const context = canvas?.getContext("2d");
if (!wrapper || !canvas || !context) return;
const step = size + gap;
let cols = 0;
let rows = 0;
let opacities = new Float32Array(0);
let frame = 0;
let last = 0;
let visible = true;
function draw() {
context!.clearRect(0, 0, canvas!.width, canvas!.height);
context!.fillStyle = color;
for (let i = 0; i < opacities.length; i++) {
context!.globalAlpha = opacities[i];
context!.fillRect((i % cols) * step, Math.floor(i / cols) * step, size, size);
}
context!.globalAlpha = 1;
}
function resize() {
const ratio = window.devicePixelRatio || 1;
const { offsetWidth: width, offsetHeight: height } = wrapper!;
canvas!.width = width * ratio;
canvas!.height = height * ratio;
context!.setTransform(ratio, 0, 0, ratio, 0, 0);
cols = Math.ceil(width / step);
rows = Math.ceil(height / step);
opacities = Float32Array.from({ length: cols * rows }, () => Math.random() * maxOpacity);
draw();
}
function tick(time: number) {
const seconds = last ? (time - last) / 1000 : 0;
last = time;
for (let i = 0; i < opacities.length; i++) {
if (Math.random() < chance * seconds) opacities[i] = Math.random() * maxOpacity;
}
draw();
frame = visible ? requestAnimationFrame(tick) : 0;
}
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(wrapper);
// stop drawing while scrolled out of view
const intersectionObserver = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
if (visible && !reduceMotion && !frame) {
last = 0;
frame = requestAnimationFrame(tick);
}
});
intersectionObserver.observe(wrapper);
return () => {
cancelAnimationFrame(frame);
resizeObserver.disconnect();
intersectionObserver.disconnect();
};
}, [size, gap, chance, maxOpacity, color, reduceMotion]);
return (
<div ref={wrapperRef} className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}>
<canvas ref={canvasRef} aria-hidden className="absolute inset-0 -z-10 size-full" />
{/* soft edges so the grid melts into the page */}
<div aria-hidden className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_center,transparent_40%,#0C1010)]" />
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| size | number | 4 | Square size in pixels. |
| gap | number | 6 | Space between squares in pixels. |
| chance | number | 0.3 | 0–1. Chance each square changes brightness per second. |
| maxOpacity | number | 0.35 | 0–1. Brightest a square gets. |
| color | string | "#2E9098" | Square colour. |
Cursor Trail
A soft ribbon of light behind the pointer, across the window or inside one section.
To infinity
Infinite Perspective Grid
A true 3D floor of glowing lines that recedes forever to a lit horizon, with an optional ceiling for a tunnel.
Ambient by default
Aurora Background
Drifting, blurred color fields for hero sections and empty states.