Grid Loader
Grid Loader é um componente de feedback para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
feedback
O que é
Grid Loader é um componente de feedback para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
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 };