A flickering grid background made with SVGs
1import { FlickeringGrid } from "@/components/backgrounds/pxl/flickering-grid";2
3export default function FlickeringGridDemo() {4 return (5 <FlickeringGrid6 />7 );8}Installation
Section titled “Installation”pnpm dlx shadcn@latest add pxl-ui/registry/backgrounds/flickering-grid
Copy and paste the following code into your project.
1"use client";2
3import { cva, type VariantProps } from "class-variance-authority";4import {5 type HTMLAttributes,6 useCallback,7 useEffect,8 useLayoutEffect,9 useRef,10 useState,11} from "react";12
13import { cn } from "@/lib/utils";14
15const flickeringGridVariants = cva("absolute inset-0 z-0 size-full", {16 variants: {17 variant: {18 default: "bg-background text-foreground",19 primary: "bg-primary text-primary-foreground",20 secondary: "bg-secondary text-secondary-foreground",21 info: "bg-info text-info-foreground",22 success: "bg-success text-success-foreground",23 warning: "bg-warning text-warning-foreground",24 danger: "bg-danger text-danger-foreground",25 muted: "bg-muted text-muted-foreground",26 },27 },28});29
30const flickeringGridSquareSizes = {31 default: 4,32 "4xs": 0.6,33 "3xs": 0.8,34 "2xs": 1,35 xs: 2,36 sm: 4,37 md: 6,38 lg: 8,39 xl: 10,40 "2xl": 14,41 "3xl": 20,42} as const;43
44function FlickeringGrid({45 className,46 flickerChance = 0.3,47 gridGap = 6,48 height,49 maxOpacity = 0.3,50 size = "default",51 variant = "default",52 width,53 ...props54}: HTMLAttributes<HTMLDivElement> &55 VariantProps<typeof flickeringGridVariants> & {56 flickerChance?: number;57 gridGap?: number;58 height?: number;59 maxOpacity?: number;60 width?: number;61 size?: keyof typeof flickeringGridSquareSizes;62 }) {63 const canvasRef = useRef<HTMLCanvasElement>(null);64 const containerRef = useRef<HTMLDivElement>(null);65 const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });66 const [isInView, setIsInView] = useState(false);67 const rgbaRef = useRef({ r: 107, g: 114, b: 128, a: 1 });68 const [computedColor, setComputedColor] = useState("rgba(107, 114, 128, 1)");69 const squareSize = flickeringGridSquareSizes[size];70
71 // biome-ignore lint/correctness/useExhaustiveDependencies: computed styles might change on font and variant changes72 useLayoutEffect(73 function resolveComputedStyles() {74 if (!containerRef.current) return;75
76 const computedStyle = getComputedStyle(containerRef.current);77 setComputedColor(computedStyle.color);78 },79 [variant],80 );81
82 // Resolve the CSS color string to RGBA (handles hex, rgb, hsl, oklch, ...).83 useEffect(84 function resolveRGBATextColor() {85 const probe = document.createElement("canvas");86 probe.width = 1;87 probe.height = 1;88 const probeCtx = probe.getContext("2d");89 if (!probeCtx) return;90 // Seed with the default so an invalid color falls back to it: the 2d91 // context keeps the previous fillStyle when assigned an invalid value92 // instead of silently turning black.93 probeCtx.fillStyle = "#6B7280";94 probeCtx.fillStyle = computedColor;95 probeCtx.fillRect(0, 0, 1, 1);96 const [r, g, b, a] = probeCtx.getImageData(0, 0, 1, 1).data;97 rgbaRef.current = { r, g, b, a: a / 255 };98 },99 [computedColor],100 );101
102 const setupCanvas = useCallback(103 (canvas: HTMLCanvasElement, width: number, height: number) => {104 const dpr = window.devicePixelRatio || 1;105 canvas.width = width * dpr;106 canvas.height = height * dpr;107 canvas.style.width = `${width}px`;108 canvas.style.height = `${height}px`;109 const cols = Math.ceil(width / (squareSize + gridGap));110 const rows = Math.ceil(height / (squareSize + gridGap));111
112 const squares = new Float32Array(cols * rows);113 for (let i = 0; i < squares.length; i++) {114 squares[i] = Math.random() * maxOpacity;115 }116
117 return { cols, rows, squares, dpr };118 },119 [squareSize, gridGap, maxOpacity],120 );121
122 const updateSquares = useCallback(123 (squares: Float32Array, deltaTime: number) => {124 for (let i = 0; i < squares.length; i++) {125 if (Math.random() < flickerChance * deltaTime) {126 squares[i] = Math.random() * maxOpacity;127 }128 }129 },130 [flickerChance, maxOpacity],131 );132
133 const drawGrid = useCallback(134 (135 ctx: CanvasRenderingContext2D,136 width: number,137 height: number,138 cols: number,139 rows: number,140 squares: Float32Array,141 dpr: number,142 ) => {143 ctx.clearRect(0, 0, width, height);144 ctx.fillStyle = "transparent";145 ctx.fillRect(0, 0, width, height);146
147 const { r, g, b } = rgbaRef.current;148
149 for (let i = 0; i < cols; i++) {150 for (let j = 0; j < rows; j++) {151 const opacity = squares[i * rows + j];152 ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`;153 ctx.fillRect(154 i * (squareSize + gridGap) * dpr,155 j * (squareSize + gridGap) * dpr,156 squareSize * dpr,157 squareSize * dpr,158 );159 }160 }161 },162 [squareSize, gridGap],163 );164
165 useEffect(() => {166 const canvas = canvasRef.current;167 const container = containerRef.current;168 const ctx = canvas?.getContext("2d") ?? null;169 let animationFrameId: number | null = null;170 let resizeObserver: ResizeObserver | null = null;171 let intersectionObserver: IntersectionObserver | null = null;172 let gridParams: ReturnType<typeof setupCanvas> | null = null;173
174 if (canvas && container && ctx) {175 const updateCanvasSize = () => {176 const newWidth = width || container.clientWidth;177 const newHeight = height || container.clientHeight;178 setCanvasSize({ width: newWidth, height: newHeight });179 gridParams = setupCanvas(canvas, newWidth, newHeight);180 };181
182 updateCanvasSize();183
184 let lastTime = 0;185 const animate = (time: number) => {186 if (!isInView || !gridParams) return;187
188 const deltaTime = (time - lastTime) / 1000;189 lastTime = time;190
191 updateSquares(gridParams.squares, deltaTime);192 drawGrid(193 ctx,194 canvas.width,195 canvas.height,196 gridParams.cols,197 gridParams.rows,198 gridParams.squares,199 gridParams.dpr,200 );201 animationFrameId = requestAnimationFrame(animate);202 };203
204 resizeObserver = new ResizeObserver(() => {205 updateCanvasSize();206 });207 resizeObserver.observe(container);208
209 intersectionObserver = new IntersectionObserver(210 ([entry]) => {211 setIsInView(entry.isIntersecting);212 },213 { threshold: 0 },214 );215 intersectionObserver.observe(canvas);216
217 if (isInView) {218 animationFrameId = requestAnimationFrame(animate);219 }220 }221
222 return () => {223 if (animationFrameId !== null) {224 cancelAnimationFrame(animationFrameId);225 }226 if (resizeObserver) {227 resizeObserver.disconnect();228 }229 if (intersectionObserver) {230 intersectionObserver.disconnect();231 }232 };233 }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);234
235 return (236 <div237 ref={containerRef}238 className={cn(flickeringGridVariants({ variant }), className)}239 {...props}240 >241 <canvas242 ref={canvasRef}243 className="pointer-events-none"244 style={{245 width: canvasSize.width,246 height: canvasSize.height,247 }}248 />249 </div>250 );251}252
253export { FlickeringGrid };Update the import paths to match your project setup.
import { FlickeringGrid } from "@/components/backgrounds/pxl/flickering-grid"<FlickeringGrid />Use the size prop to change the size of the squares.
1"use client";2
3import { FlickeringGrid } from "@/components/backgrounds/pxl/flickering-grid";4
5export default function FlickeringGridSizes() {6 return (7 <FlickeringGrid8 size="lg" />9 );10}Variants
Section titled “Variants”Use the variant prop to change the colors of the grid.
1"use client";2
3import { FlickeringGrid } from "@/components/backgrounds/pxl/flickering-grid";4
5export default function FlickeringGridVariants() {6 return (7 <div className="flex flex-wrap size-full items-center justify-center gap-2">8 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">9 <FlickeringGrid10 variant="muted"11 />12 </div>13 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">14 <FlickeringGrid15 variant="primary"16 />17 </div>18 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">19 <FlickeringGrid20 variant="info"21 />22 </div>23 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">24 <FlickeringGrid25 variant="success"26 />27 </div>28 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">29 <FlickeringGrid30 variant="warning"31 />32 </div>33 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">34 <FlickeringGrid35 variant="danger"36 />37 </div>38 </div>39 );40}Rounded
Section titled “Rounded”1import { FlickeringGrid } from "@/components/backgrounds/pxl/flickering-grid";2
3export default function FlickeringGridRoundedDemo() {4 return (5 <FlickeringGrid6 className="mask-[radial-gradient(200px_circle_at_center,white,transparent)]"7 />8 );9}API Reference
Section titled “API Reference”FlickeringGrid
Section titled “FlickeringGrid”| Prop | Type | Default | Description |
|---|---|---|---|
size |
"default" | "4xs" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" |
"default" |
Size of the squares |
variant |
"default" | "primary" | "secondary" | "muted" | "success" | "warning" | "danger" |
"default" |
Colors of the grids |
gridGap |
number |
6 |
Gap between squares in the grid |
flickerChance |
number |
0.3 |
Probability of a square flickering |
className |
string |
- |
Additional CSS classes for the canvas |
maxOpacity |
number |
0.2 |
Maximum opacity of the squares |