Grid Loader
Grid Loader is a React and Tailwind CSS feedback component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
feedback
What it is
Grid Loader is a React and Tailwind CSS feedback component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Grid Loader
"use client";
import { cn } from "@/lib/utils";
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useMemo, useState } from "react";
// Grid matrix type: 3x3 array of 0s and 1s
export type GridMatrix = [
[0 | 1, 0 | 1, 0 | 1],
[0 | 1, 0 | 1, 0 | 1],
[0 | 1, 0 | 1, 0 | 1],
];
// All preset pattern names
export type PresetPattern =
// Solo (single cell)
| "solo-center"
| "solo-tl"
| "solo-tr"
| "solo-bl"
| "solo-br"
// Horizontal lines
| "line-h-top"
| "line-h-mid"
| "line-h-bot"
// Vertical lines
| "line-v-left"
| "line-v-mid"
| "line-v-right"
// Diagonals
| "line-diag-1"
| "line-diag-2"
// Corners
| "corners-only"
| "corners-sync"
| "corners"
// Plus
| "plus-hollow"
| "plus-full"
// L-shapes
| "L-tl"
| "L-tr"
| "L-bl"
| "L-br"
// T-shapes
| "T-top"
| "T-bot"
| "T-left"
| "T-right"
// Duo
| "duo-h"
| "duo-v"
| "duo-diag"
// Frame
| "frame"
| "frame-sync"
// Sparse
| "sparse-1"
| "sparse-2"
| "sparse-3"
// Waves
| "wave-lr"
| "wave-rl"
| "wave-tb"
| "wave-bt"
// Diagonal quadrants
| "diagonal-tl"
| "diagonal-tr"
| "diagonal-bl"
| "diagonal-br"
// Ripple
| "ripple-out"
| "ripple-in"
// Shapes
| "cross"
| "x-shape"
| "diamond"
// Stripes
| "stripes-h"
| "stripes-v"
// Patterns
| "checkerboard"
| "rows-alt"
// Spirals
| "spiral-cw"
| "spiral-ccw"
// Snake
| "snake"
| "snake-rev"
// Rain
| "rain"
| "rain-rev"
// Effects
| "waterfall"
| "breathing"
| "heartbeat"
| "twinkle"
| "sparkle"
| "chaos"
// Edge
| "edge-cw"
| "border";
export interface GridLoaderProps {
/** Blur amount in pixels — creates a soft glow effect */
blur?: number;
/** Additional CSS classes */
className?: string;
/** Color preset or custom CSS color */
color?: "white" | "red" | "blue" | "green" | "amber" | string;
/** Gap between cells in pixels */
gap?: number;
/** Animation mode */
mode?: "pulse" | "sequence" | "stagger";
/** Pattern to display - preset name or custom 3x3 matrix */
pattern?: PresetPattern | GridMatrix;
/** Use rounded (circular) cells instead of square */
rounded?: boolean;
/** Array of patterns for sequence mode */
sequence?: Array<PresetPattern | GridMatrix>;
/** Size preset or pixel value */
size?: "sm" | "md" | "lg" | "xl" | number;
/** Animation speed */
speed?: "slow" | "normal" | "fast";
/** Disable animation and show static pattern */
static?: boolean;
}
// Preset patterns as 3x3 matrices
const PATTERNS: Record<PresetPattern, GridMatrix> = {
border: [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
],
breathing: [
[0, 0, 0],
[0, 1, 0],
[0, 0, 0],
],
chaos: [
[1, 0, 1],
[0, 1, 0],
[1, 0, 0],
],
// Patterns
checkerboard: [
[1, 0, 1],
[0, 1, 0],
[1, 0, 1],
],
corners: [
[1, 0, 1],
[0, 0, 0],
[1, 0, 1],
],
// Corners
"corners-only": [
[1, 0, 1],
[0, 0, 0],
[1, 0, 1],
],
"corners-sync": [
[1, 0, 1],
[0, 0, 0],
[1, 0, 1],
],
// Shapes
cross: [
[0, 1, 0],
[1, 1, 1],
[0, 1, 0],
],
"diagonal-bl": [
[0, 0, 0],
[1, 1, 0],
[1, 1, 0],
],
"diagonal-br": [
[0, 0, 0],
[0, 1, 1],
[0, 1, 1],
],
// Diagonal quadrants
"diagonal-tl": [
[1, 1, 0],
[1, 1, 0],
[0, 0, 0],
],
"diagonal-tr": [
[0, 1, 1],
[0, 1, 1],
[0, 0, 0],
],
diamond: [
[0, 1, 0],
[1, 0, 1],
[0, 1, 0],
],
"duo-diag": [
[1, 0, 0],
[0, 1, 0],
[0, 0, 0],
],
// Duo (two cells)
"duo-h": [
[0, 0, 0],
[1, 1, 0],
[0, 0, 0],
],
"duo-v": [
[0, 1, 0],
[0, 1, 0],
[0, 0, 0],
],
// Edge
"edge-cw": [
[1, 1, 1],
[0, 0, 0],
[0, 0, 0],
],
// Frame
frame: [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
],
"frame-sync": [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
],
heartbeat: [
[0, 1, 0],
[1, 1, 1],
[0, 1, 0],
],
"L-bl": [
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
],
"L-br": [
[0, 0, 0],
[0, 0, 1],
[0, 1, 1],
],
// L-shapes
"L-tl": [
[1, 1, 0],
[1, 0, 0],
[0, 0, 0],
],
"L-tr": [
[0, 1, 1],
[0, 0, 1],
[0, 0, 0],
],
// Diagonals
"line-diag-1": [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
],
"line-diag-2": [
[0, 0, 1],
[0, 1, 0],
[1, 0, 0],
],
"line-h-bot": [
[0, 0, 0],
[0, 0, 0],
[1, 1, 1],
],
"line-h-mid": [
[0, 0, 0],
[1, 1, 1],
[0, 0, 0],
],
// Horizontal lines
"line-h-top": [
[1, 1, 1],
[0, 0, 0],
[0, 0, 0],
],
// Vertical lines
"line-v-left": [
[1, 0, 0],
[1, 0, 0],
[1, 0, 0],
],
"line-v-mid": [
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
],
"line-v-right": [
[0, 0, 1],
[0, 0, 1],
[0, 0, 1],
],
"plus-full": [
[0, 1, 0],
[1, 1, 1],
[0, 1, 0],
],
// Plus
"plus-hollow": [
[0, 1, 0],
[1, 0, 1],
[0, 1, 0],
],
// Rain
rain: [
[0, 1, 0],
[0, 1, 0],
[0, 0, 0],
],
"rain-rev": [
[0, 0, 0],
[0, 1, 0],
[0, 1, 0],
],
"ripple-in": [
[0, 0, 0],
[0, 1, 0],
[0, 0, 0],
],
// Ripple
"ripple-out": [
[1, 1, 1],
[1, 0, 1],
[1, 1, 1],
],
"rows-alt": [
[1, 1, 1],
[0, 0, 0],
[1, 1, 1],
],
// Snake
snake: [
[1, 1, 0],
[0, 1, 0],
[0, 0, 0],
],
"snake-rev": [
[0, 0, 0],
[0, 1, 0],
[0, 1, 1],
],
"solo-bl": [
[0, 0, 0],
[0, 0, 0],
[1, 0, 0],
],
"solo-br": [
[0, 0, 0],
[0, 0, 0],
[0, 0, 1],
],
// Solo (single cell)
"solo-center": [
[0, 0, 0],
[0, 1, 0],
[0, 0, 0],
],
"solo-tl": [
[1, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
"solo-tr": [
[0, 0, 1],
[0, 0, 0],
[0, 0, 0],
],
sparkle: [
[1, 0, 1],
[0, 1, 0],
[1, 0, 1],
],
// Sparse
"sparse-1": [
[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
],
"sparse-2": [
[0, 1, 0],
[1, 0, 0],
[0, 0, 1],
],
"sparse-3": [
[0, 0, 1],
[0, 1, 0],
[1, 0, 0],
],
"spiral-ccw": [
[1, 1, 1],
[1, 0, 0],
[1, 0, 0],
],
// Spirals
"spiral-cw": [
[1, 1, 1],
[0, 0, 1],
[0, 0, 1],
],
// Stripes
"stripes-h": [
[1, 1, 1],
[0, 0, 0],
[1, 1, 1],
],
"stripes-v": [
[1, 0, 1],
[1, 0, 1],
[1, 0, 1],
],
"T-bot": [
[0, 0, 0],
[0, 1, 0],
[1, 1, 1],
],
"T-left": [
[1, 0, 0],
[1, 1, 0],
[1, 0, 0],
],
"T-right": [
[0, 0, 1],
[0, 1, 1],
[0, 0, 1],
],
// T-shapes
"T-top": [
[1, 1, 1],
[0, 1, 0],
[0, 0, 0],
],
twinkle: [
[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
],
// Effects
waterfall: [
[1, 1, 0],
[0, 0, 0],
[0, 0, 0],
],
"wave-bt": [
[0, 0, 0],
[1, 1, 1],
[1, 1, 1],
],
// Waves
"wave-lr": [
[1, 1, 0],
[1, 1, 0],
[1, 1, 0],
],
"wave-rl": [
[0, 1, 1],
[0, 1, 1],
[0, 1, 1],
],
"wave-tb": [
[1, 1, 1],
[1, 1, 1],
[0, 0, 0],
],
"x-shape": [
[1, 0, 1],
[0, 1, 0],
[1, 0, 1],
],
};
// Color presets
const COLORS: Record<string, string> = {
amber: "#fbbf24",
blue: "#38bdf8",
green: "#4ade80",
red: "#f87171",
white: "#f5f5f4",
};
// Size presets in pixels
const SIZES: Record<string, number> = {
lg: 48,
md: 32,
sm: 24,
xl: 64,
};
// Speed presets in milliseconds
const SPEEDS: Record<string, number> = {
fast: 400,
normal: 800,
slow: 1500,
};
// Helper to resolve pattern to GridMatrix
const resolvePattern = (
pattern: PresetPattern | GridMatrix | undefined
): GridMatrix => {
if (!pattern) {
return PATTERNS["plus-hollow"];
}
if (typeof pattern === "string") {
return PATTERNS[pattern] ?? PATTERNS["plus-hollow"];
}
return pattern;
};
// Helper to resolve color
const resolveColor = (color: string | undefined): string => {
if (!color) {
return COLORS.white;
}
return COLORS[color] ?? color;
};
// Helper to resolve size
const resolveSize = (
size: "sm" | "md" | "lg" | "xl" | number | undefined
): number => {
if (size === undefined) {
return SIZES.md;
}
if (typeof size === "number") {
return size;
}
return SIZES[size] ?? SIZES.md;
};
// Cell component for individual grid cells
const GridCell = ({
active,
color,
cellSize,
animationDelay,
mode,
cycleDuration,
shouldReduceMotion,
rounded,
}: {
active: boolean;
color: string;
cellSize: number;
animationDelay: number;
mode: "pulse" | "sequence" | "stagger";
cycleDuration: number;
shouldReduceMotion: boolean | null;
rounded?: boolean;
}) => {
const glowSize = cellSize * 0.8;
const borderRadius = rounded ? "50%" : undefined;
if (!active) {
return (
<div
style={{
height: cellSize,
width: cellSize,
}}
/>
);
}
// For reduced motion, show static cells
if (shouldReduceMotion) {
return (
<div
style={{
backgroundColor: color,
borderRadius,
boxShadow: `
0 0 ${glowSize * 0.3}px ${color},
0 0 ${glowSize * 0.6}px ${color}40,
0 0 ${glowSize * 1.2}px ${color}20
`,
height: cellSize,
width: cellSize,
}}
/>
);
}
const pulseAnimation = {
opacity: [0.4, 1, 0.4],
scale: [0.95, 1, 0.95],
};
const staggerAnimation = {
opacity: [0, 1, 1, 0],
scale: [0.8, 1, 1, 0.8],
};
return (
<motion.div
animate={mode === "stagger" ? staggerAnimation : pulseAnimation}
style={{
backgroundColor: color,
borderRadius,
boxShadow: `
0 0 ${glowSize * 0.3}px ${color},
0 0 ${glowSize * 0.6}px ${color}40,
0 0 ${glowSize * 1.2}px ${color}20
`,
height: cellSize,
width: cellSize,
}}
transition={{
delay: mode === "stagger" ? animationDelay : 0,
duration: cycleDuration / 1000,
ease: [0.645, 0.045, 0.355, 1],
repeat: Number.POSITIVE_INFINITY,
times: mode === "stagger" ? [0, 0.2, 0.8, 1] : undefined,
}}
/>
);
};
const GridLoader = ({
pattern,
mode = "pulse",
sequence,
speed = "normal",
color,
size,
blur,
gap: gapProp,
rounded,
static: isStatic,
className,
}: GridLoaderProps) => {
const shouldReduceMotion = useReducedMotion();
const [sequenceIndex, setSequenceIndex] = useState(0);
const sizeInPx = resolveSize(size);
const gapSize = gapProp ?? 0;
const cellSize = (sizeInPx - gapSize * 2) / 3;
const resolvedColor = resolveColor(color);
const cycleDuration = SPEEDS[speed] ?? SPEEDS.normal;
const disableAnimation = isStatic || shouldReduceMotion;
// Handle sequence mode
useEffect(() => {
if (mode !== "sequence" || !sequence || sequence.length === 0) {
return;
}
if (disableAnimation) {
return;
}
const interval = setInterval(() => {
setSequenceIndex((prev) => (prev + 1) % sequence.length);
}, cycleDuration);
return () => clearInterval(interval);
}, [mode, sequence, cycleDuration, disableAnimation]);
// Determine current grid to display
const currentGrid = useMemo(() => {
if (mode === "sequence" && sequence && sequence.length > 0) {
return resolvePattern(sequence[sequenceIndex]);
}
return resolvePattern(pattern);
}, [mode, sequence, sequenceIndex, pattern]);
// Flatten grid for rendering
const cells = currentGrid.flat();
// Calculate stagger delays based on active cells
const staggerDelays = useMemo(() => {
if (mode !== "stagger") {
return cells.map(() => 0);
}
const activeCells = cells.reduce<number[]>((acc, cell, idx) => {
if (cell === 1) {
acc.push(idx);
}
return acc;
}, []);
const delayPerCell = cycleDuration / 1000 / (activeCells.length + 2);
return cells.map((cell, idx) => {
if (cell === 0) {
return 0;
}
const activeIndex = activeCells.indexOf(idx);
return activeIndex * delayPerCell;
});
}, [cells, mode, cycleDuration]);
// Generate stable keys for grid positions (0-8 in a 3x3 grid)
const cellKeys = ["tl", "tm", "tr", "ml", "mm", "mr", "bl", "bm", "br"];
return (
<output
aria-label="Loading"
className={cn("grid grid-cols-3", className)}
style={{
filter: blur ? `blur(${blur}px)` : undefined,
gap: gapSize,
height: sizeInPx,
width: sizeInPx,
}}
>
{cells.map((active, idx) => (
<GridCell
active={active === 1}
animationDelay={staggerDelays[idx]}
cellSize={cellSize}
color={resolvedColor}
cycleDuration={cycleDuration}
key={cellKeys[idx]}
mode={mode}
rounded={rounded}
shouldReduceMotion={disableAnimation}
/>
))}
</output>
);
};
export default GridLoader;
// Export patterns for advanced usage
export { COLORS, PATTERNS, SIZES };