Animated Grid Pattern
An animated background grid pattern made with SVGs, fully customizable using Tailwind CSS.
Installation
Copy and paste the following code into your project:
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref, useId, watch } from "vue";
import { cn } from "@/lib/utils";
interface Square {
id: number;
pos: [number, number];
iteration: number;
}
interface AnimatedGridPatternProps {
width?: number;
height?: number;
x?: number;
y?: number;
strokeDasharray?: number;
numSquares?: number;
maxOpacity?: number;
duration?: number;
repeatDelay?: number;
class?: string;
}
const props = withDefaults(defineProps<AnimatedGridPatternProps>(), {
width: 40,
height: 40,
x: -1,
y: -1,
strokeDasharray: 0,
numSquares: 50,
maxOpacity: 0.5,
duration: 4,
repeatDelay: 0.5,
});
const id = `pattern-${useId()}`;
const containerRef = ref<SVGSVGElement | null>(null);
const dimensions = ref({ width: 0, height: 0 });
const squares = ref<Square[]>([]);
function getPos(): [number, number] {
return [
Math.floor((Math.random() * dimensions.value.width) / props.width),
Math.floor((Math.random() * dimensions.value.height) / props.height),
];
}
function generateSquares(count: number): Square[] {
return Array.from({ length: count }, (_, i) => ({
id: i,
pos: getPos(),
iteration: 0,
}));
}
function updateSquarePosition(squareId: number) {
const current = squares.value[squareId];
if (!current || current.id !== squareId) {
return;
}
squares.value[squareId] = {
...current,
pos: getPos(),
iteration: current.iteration + 1,
};
}
watch(
() => [dimensions.value.width, dimensions.value.height, props.numSquares] as const,
([width, height]) => {
if (width && height) {
squares.value = generateSquares(props.numSquares);
}
},
);
function animateRect(el: Element | null, square: Square, index: number) {
const node = el as (SVGRectElement & { animate?: SVGRectElement["animate"] }) | null;
if (!node || typeof node.animate !== "function") {
return;
}
const total = props.duration * 2 + props.repeatDelay;
const forward = props.duration / total;
const hold = (props.duration + props.repeatDelay) / total;
const animation = node.animate(
[
{ opacity: 0, offset: 0 },
{ opacity: props.maxOpacity, offset: forward },
{ opacity: props.maxOpacity, offset: hold },
{ opacity: 0, offset: 1 },
],
{
duration: total * 1000,
delay: index * 100,
easing: "ease-out",
fill: "forwards",
},
);
animation.onfinish = () => updateSquarePosition(square.id);
}
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
const element = containerRef.value;
if (!element || typeof ResizeObserver === "undefined") {
return;
}
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const nextWidth = entry.contentRect.width;
const nextHeight = entry.contentRect.height;
if (dimensions.value.width !== nextWidth || dimensions.value.height !== nextHeight) {
dimensions.value = { width: nextWidth, height: nextHeight };
}
}
});
resizeObserver.observe(element);
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
</script>
<template>
<svg
ref="containerRef"
aria-hidden="true"
:class="
cn(
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
props.class,
)
"
>
<defs>
<pattern
:id="id"
:width="props.width"
:height="props.height"
patternUnits="userSpaceOnUse"
:x="props.x"
:y="props.y"
>
<path
:d="`M.5 ${props.height}V.5H${props.width}`"
fill="none"
:stroke-dasharray="props.strokeDasharray"
/>
</pattern>
</defs>
<rect width="100%" height="100%" :fill="`url(#${id})`" />
<svg :x="props.x" :y="props.y" class="overflow-visible">
<rect
v-for="(square, index) in squares"
:key="`${square.id}-${square.iteration}`"
:ref="(el) => animateRect(el as Element | null, square, index)"
:width="props.width - 1"
:height="props.height - 1"
:x="square.pos[0] * props.width + 1"
:y="square.pos[1] * props.height + 1"
fill="currentColor"
stroke-width="0"
:style="{ opacity: 0 }"
/>
</svg>
</svg>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | 40 | Width of the grid pattern cell |
height | number | 40 | Height of the grid pattern cell |
x | number | -1 | X offset of the pattern |
y | number | -1 | Y offset of the pattern |
strokeDasharray | number | 0 | Stroke dash array of the grid lines |
numSquares | number | 50 | Number of animated squares in the pattern |
maxOpacity | number | 0.5 | Maximum opacity each square animates to |
duration | number | 4 | Duration (in seconds) of a single fade direction |
repeatDelay | number | 0.5 | Delay (in seconds) before a square fades back out |
class | string | - | Additional classes to be added to the pattern |