UI Components Medium
Interactive Grid Pattern
A canvas-based grid pattern that reacts to mouse movement, illuminating cells near the cursor with distance-based brightness falloff.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #000;
overflow: hidden;
}
.grid-wrapper {
position: relative;
width: 100%;
height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
overflow: hidden;
}
#grid-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.grid-fade {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 60% 60% at 50% 50%, transparent 20%, #0a0a0a 75%);
pointer-events: none;
}
.grid-content {
position: relative;
z-index: 10;
text-align: center;
color: #f1f5f9;
pointer-events: none;
}
.grid-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #d1fae5 0%, #34d399 50%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.grid-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
font-weight: 400;
}// Interactive Grid Pattern — cells illuminate based on mouse proximity
(function () {
"use strict";
const canvas = document.getElementById("grid-canvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
const CELL_SIZE = 32;
const GAP = 2;
const CORNER_RADIUS = 3;
const ILLUMINATION_RADIUS = 200;
const TRAIL_RADIUS = 120;
let mouseX = -1000;
let mouseY = -1000;
let targetX = -1000;
let targetY = -1000;
let cols = 0;
let rows = 0;
let dpr = 1;
let animId;
function resize() {
dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
ctx.scale(dpr, dpr);
cols = Math.ceil(window.innerWidth / (CELL_SIZE + GAP)) + 1;
rows = Math.ceil(window.innerHeight / (CELL_SIZE + GAP)) + 1;
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function draw() {
// Smooth mouse following
mouseX = lerp(mouseX, targetX, 0.15);
mouseY = lerp(mouseY, targetY, 0.15);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * (CELL_SIZE + GAP);
const y = r * (CELL_SIZE + GAP);
const centerX = x + CELL_SIZE / 2;
const centerY = y + CELL_SIZE / 2;
const dx = centerX - mouseX;
const dy = centerY - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Intensity falls off smoothly
const intensity = Math.max(0, 1 - dist / ILLUMINATION_RADIUS);
const smoothIntensity = intensity * intensity; // Quadratic falloff
// Trail glow (softer, wider)
const trailIntensity = Math.max(0, 1 - dist / (ILLUMINATION_RADIUS + TRAIL_RADIUS));
const smoothTrail = trailIntensity * trailIntensity * 0.3;
const finalIntensity = Math.min(1, smoothIntensity + smoothTrail);
// Base: dim grid line / Lit: bright emerald
const baseR = 255,
baseG = 255,
baseB = 255,
baseA = 0.04;
const glowR = 52,
glowG = 211,
glowB = 153;
const alpha = baseA + finalIntensity * 0.55;
const red = Math.round(lerp(baseR * baseA, glowR, finalIntensity) / Math.max(alpha, 0.01));
const green = Math.round(
lerp(baseG * baseA, glowG, finalIntensity) / Math.max(alpha, 0.01)
);
const blue = Math.round(lerp(baseB * baseA, glowB, finalIntensity) / Math.max(alpha, 0.01));
ctx.fillStyle = `rgba(${glowR}, ${glowG}, ${glowB}, ${alpha.toFixed(3)})`;
if (finalIntensity > 0.01) {
// Draw brighter cells
ctx.shadowColor = `rgba(${glowR}, ${glowG}, ${glowB}, ${(finalIntensity * 0.6).toFixed(3)})`;
ctx.shadowBlur = finalIntensity * 12;
} else {
// Dim base cells
ctx.fillStyle = `rgba(255, 255, 255, 0.04)`;
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
}
roundRect(x, y, CELL_SIZE, CELL_SIZE, CORNER_RADIUS);
ctx.fill();
// Reset shadow
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
// Subtle border for lit cells
if (finalIntensity > 0.1) {
ctx.strokeStyle = `rgba(${glowR}, ${glowG}, ${glowB}, ${(finalIntensity * 0.3).toFixed(3)})`;
ctx.lineWidth = 0.5;
roundRect(x, y, CELL_SIZE, CELL_SIZE, CORNER_RADIUS);
ctx.stroke();
}
}
}
animId = requestAnimationFrame(draw);
}
canvas.parentElement.addEventListener("mousemove", (e) => {
targetX = e.clientX;
targetY = e.clientY;
});
canvas.parentElement.addEventListener("mouseleave", () => {
targetX = -1000;
targetY = -1000;
});
// Touch support
canvas.parentElement.addEventListener(
"touchmove",
(e) => {
const touch = e.touches[0];
targetX = touch.clientX;
targetY = touch.clientY;
},
{ passive: true }
);
canvas.parentElement.addEventListener("touchend", () => {
targetX = -1000;
targetY = -1000;
});
resize();
draw();
let resizeTimer;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Interactive Grid Pattern</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="grid-wrapper">
<canvas id="grid-canvas"></canvas>
<div class="grid-fade"></div>
<div class="grid-content">
<h1 class="grid-title">Interactive Grid</h1>
<p class="grid-subtitle">Move your mouse to illuminate nearby cells</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useRef, useCallback } from "react";
interface InteractiveGridPatternProps {
cellSize?: number;
gap?: number;
cornerRadius?: number;
illuminationRadius?: number;
trailRadius?: number;
glowColor?: [number, number, number];
className?: string;
}
export function InteractiveGridPattern({
cellSize = 32,
gap = 2,
cornerRadius = 3,
illuminationRadius = 200,
trailRadius = 120,
glowColor = [52, 211, 153],
className = "",
}: InteractiveGridPatternProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const mouseRef = useRef({ x: -1000, y: -1000 });
const targetRef = useRef({ x: -1000, y: -1000 });
const animRef = useRef<number>(0);
const gridRef = useRef({ cols: 0, rows: 0, dpr: 1 });
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const roundRect = useCallback(
(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) => {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
},
[]
);
const resize = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const w = parent.clientWidth;
const h = parent.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
gridRef.current = {
cols: Math.ceil(w / (cellSize + gap)) + 1,
rows: Math.ceil(h / (cellSize + gap)) + 1,
dpr,
};
}, [cellSize, gap]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
resize();
const [gR, gG, gB] = glowColor;
function draw() {
const mouse = mouseRef.current;
const target = targetRef.current;
mouse.x = lerp(mouse.x, target.x, 0.15);
mouse.y = lerp(mouse.y, target.y, 0.15);
const { cols, rows, dpr } = gridRef.current;
const w = canvas!.width / dpr;
const h = canvas!.height / dpr;
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx!.clearRect(0, 0, w, h);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * (cellSize + gap);
const y = r * (cellSize + gap);
const cx = x + cellSize / 2;
const cy = y + cellSize / 2;
const dx = cx - mouse.x;
const dy = cy - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const intensity = Math.max(0, 1 - dist / illuminationRadius);
const smoothIntensity = intensity * intensity;
const trailI = Math.max(0, 1 - dist / (illuminationRadius + trailRadius));
const smoothTrail = trailI * trailI * 0.3;
const finalIntensity = Math.min(1, smoothIntensity + smoothTrail);
const alpha = 0.04 + finalIntensity * 0.55;
if (finalIntensity > 0.01) {
ctx!.fillStyle = `rgba(${gR}, ${gG}, ${gB}, ${alpha.toFixed(3)})`;
ctx!.shadowColor = `rgba(${gR}, ${gG}, ${gB}, ${(finalIntensity * 0.6).toFixed(3)})`;
ctx!.shadowBlur = finalIntensity * 12;
} else {
ctx!.fillStyle = "rgba(255, 255, 255, 0.04)";
ctx!.shadowColor = "transparent";
ctx!.shadowBlur = 0;
}
roundRect(ctx!, x, y, cellSize, cellSize, cornerRadius);
ctx!.fill();
ctx!.shadowColor = "transparent";
ctx!.shadowBlur = 0;
if (finalIntensity > 0.1) {
ctx!.strokeStyle = `rgba(${gR}, ${gG}, ${gB}, ${(finalIntensity * 0.3).toFixed(3)})`;
ctx!.lineWidth = 0.5;
roundRect(ctx!, x, y, cellSize, cellSize, cornerRadius);
ctx!.stroke();
}
}
}
animRef.current = requestAnimationFrame(draw);
}
draw();
let resizeTimer: ReturnType<typeof setTimeout>;
const handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
};
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(animRef.current);
clearTimeout(resizeTimer);
window.removeEventListener("resize", handleResize);
};
}, [cellSize, gap, cornerRadius, illuminationRadius, trailRadius, glowColor, resize, roundRect]);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
targetRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
}, []);
const handleMouseLeave = useCallback(() => {
targetRef.current = { x: -1000, y: -1000 };
}, []);
const handleTouchMove = useCallback((e: React.TouchEvent) => {
const touch = e.touches[0];
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
targetRef.current = { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
}, []);
const handleTouchEnd = useCallback(() => {
targetRef.current = { x: -1000, y: -1000 };
}, []);
return (
<div
className={className}
style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden" }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<canvas
ref={canvasRef}
style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }}
/>
<div
style={{
position: "absolute",
inset: 0,
background: "radial-gradient(ellipse 60% 60% at 50% 50%, transparent 20%, #0a0a0a 75%)",
pointerEvents: "none",
}}
/>
</div>
);
}
// Demo usage
export default function InteractiveGridPatternDemo() {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
position: "relative",
}}
>
<InteractiveGridPattern cellSize={32} illuminationRadius={200} glowColor={[52, 211, 153]} />
<div
style={{
position: "absolute",
zIndex: 10,
textAlign: "center",
pointerEvents: "none",
}}
>
<h1
style={{
fontSize: "clamp(2rem, 5vw, 3.5rem)",
fontWeight: 800,
letterSpacing: "-0.03em",
background: "linear-gradient(135deg, #d1fae5 0%, #34d399 50%, #059669 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: "0.5rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Interactive Grid
</h1>
<p
style={{
fontSize: "clamp(0.875rem, 2vw, 1.125rem)",
color: "rgba(148, 163, 184, 0.8)",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Move your mouse to illuminate nearby cells
</p>
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
cellSize: { type: Number, default: 32 },
gap: { type: Number, default: 2 },
cornerRadius: { type: Number, default: 3 },
illuminationRadius: { type: Number, default: 200 },
trailRadius: { type: Number, default: 120 },
glowColor: { type: Array, default: () => [52, 211, 153] },
});
const wrapperEl = ref(null);
const canvasEl = ref(null);
let animId = 0;
const mouse = { x: -1000, y: -1000 };
const target = { x: -1000, y: -1000 };
const grid = { cols: 0, rows: 0, dpr: 1 };
function lerp(a, b, t) {
return a + (b - a) * t;
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function resize() {
const canvas = canvasEl.value;
const wrapper = wrapperEl.value;
if (!canvas || !wrapper) return;
const dpr = window.devicePixelRatio || 1;
const w = wrapper.clientWidth;
const h = wrapper.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
grid.cols = Math.ceil(w / (props.cellSize + props.gap)) + 1;
grid.rows = Math.ceil(h / (props.cellSize + props.gap)) + 1;
grid.dpr = dpr;
}
let resizeTimer;
function handleResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
}
function handleMouseMove(e) {
const rect = wrapperEl.value.getBoundingClientRect();
target.x = e.clientX - rect.left;
target.y = e.clientY - rect.top;
}
function handleMouseLeave() {
target.x = -1000;
target.y = -1000;
}
function handleTouchMove(e) {
const touch = e.touches[0];
const rect = wrapperEl.value.getBoundingClientRect();
target.x = touch.clientX - rect.left;
target.y = touch.clientY - rect.top;
}
function handleTouchEnd() {
target.x = -1000;
target.y = -1000;
}
onMounted(() => {
const canvas = canvasEl.value;
const ctx = canvas.getContext("2d");
if (!ctx) return;
resize();
const [gR, gG, gB] = props.glowColor;
function draw() {
mouse.x = lerp(mouse.x, target.x, 0.15);
mouse.y = lerp(mouse.y, target.y, 0.15);
const { cols, rows, dpr } = grid;
const w = canvas.width / dpr;
const h = canvas.height / dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * (props.cellSize + props.gap);
const y = r * (props.cellSize + props.gap);
const cx = x + props.cellSize / 2;
const cy = y + props.cellSize / 2;
const dx = cx - mouse.x;
const dy = cy - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const intensity = Math.max(0, 1 - dist / props.illuminationRadius);
const smoothIntensity = intensity * intensity;
const trailI = Math.max(0, 1 - dist / (props.illuminationRadius + props.trailRadius));
const smoothTrail = trailI * trailI * 0.3;
const finalIntensity = Math.min(1, smoothIntensity + smoothTrail);
const alpha = 0.04 + finalIntensity * 0.55;
if (finalIntensity > 0.01) {
ctx.fillStyle = `rgba(${gR}, ${gG}, ${gB}, ${alpha.toFixed(3)})`;
ctx.shadowColor = `rgba(${gR}, ${gG}, ${gB}, ${(finalIntensity * 0.6).toFixed(3)})`;
ctx.shadowBlur = finalIntensity * 12;
} else {
ctx.fillStyle = "rgba(255, 255, 255, 0.04)";
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
}
roundRect(ctx, x, y, props.cellSize, props.cellSize, props.cornerRadius);
ctx.fill();
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
if (finalIntensity > 0.1) {
ctx.strokeStyle = `rgba(${gR}, ${gG}, ${gB}, ${(finalIntensity * 0.3).toFixed(3)})`;
ctx.lineWidth = 0.5;
roundRect(ctx, x, y, props.cellSize, props.cellSize, props.cornerRadius);
ctx.stroke();
}
}
}
animId = requestAnimationFrame(draw);
}
draw();
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
cancelAnimationFrame(animId);
clearTimeout(resizeTimer);
window.removeEventListener("resize", handleResize);
});
</script>
<template>
<div class="grid-demo">
<div
ref="wrapperEl"
class="grid-wrapper"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<canvas ref="canvasEl" class="grid-canvas" />
<div class="vignette" />
</div>
<div class="label-overlay">
<h1 class="grid-title">Interactive Grid</h1>
<p class="grid-subtitle">Move your mouse to illuminate nearby cells</p>
</div>
</div>
</template>
<style scoped>
.grid-demo {
width: 100vw;
height: 100vh;
background: #0a0a0a;
display: grid;
place-items: center;
position: relative;
}
.grid-wrapper {
position: absolute;
inset: 0;
overflow: hidden;
}
.grid-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 60% 60% at 50% 50%, transparent 20%, #0a0a0a 75%);
pointer-events: none;
}
.label-overlay {
position: absolute;
z-index: 10;
text-align: center;
pointer-events: none;
}
.grid-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #d1fae5 0%, #34d399 50%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 0.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.grid-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
}
</style><script>
import { onMount, onDestroy } from "svelte";
export let cellSize = 32;
export let gap = 2;
export let cornerRadius = 3;
export let illuminationRadius = 200;
export let trailRadius = 120;
export let glowColor = [52, 211, 153];
let wrapperEl;
let canvasEl;
let animId = 0;
let mouse = { x: -1000, y: -1000 };
let target = { x: -1000, y: -1000 };
let grid = { cols: 0, rows: 0, dpr: 1 };
function lerp(a, b, t) {
return a + (b - a) * t;
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function resize() {
if (!canvasEl || !wrapperEl) return;
const dpr = window.devicePixelRatio || 1;
const w = wrapperEl.clientWidth;
const h = wrapperEl.clientHeight;
canvasEl.width = w * dpr;
canvasEl.height = h * dpr;
canvasEl.style.width = w + "px";
canvasEl.style.height = h + "px";
grid = {
cols: Math.ceil(w / (cellSize + gap)) + 1,
rows: Math.ceil(h / (cellSize + gap)) + 1,
dpr,
};
}
let resizeTimer;
function handleResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resize, 150);
}
function handleMouseMove(e) {
const rect = wrapperEl.getBoundingClientRect();
target = { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
function handleMouseLeave() {
target = { x: -1000, y: -1000 };
}
function handleTouchMove(e) {
const touch = e.touches[0];
const rect = wrapperEl.getBoundingClientRect();
target = { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
}
function handleTouchEnd() {
target = { x: -1000, y: -1000 };
}
onMount(() => {
const ctx = canvasEl.getContext("2d");
if (!ctx) return;
resize();
const [gR, gG, gB] = glowColor;
function draw() {
mouse.x = lerp(mouse.x, target.x, 0.15);
mouse.y = lerp(mouse.y, target.y, 0.15);
const { cols, rows, dpr } = grid;
const w = canvasEl.width / dpr;
const h = canvasEl.height / dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * (cellSize + gap);
const y = r * (cellSize + gap);
const cx = x + cellSize / 2;
const cy = y + cellSize / 2;
const dx = cx - mouse.x;
const dy = cy - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const intensity = Math.max(0, 1 - dist / illuminationRadius);
const smoothIntensity = intensity * intensity;
const trailI = Math.max(0, 1 - dist / (illuminationRadius + trailRadius));
const smoothTrail = trailI * trailI * 0.3;
const finalIntensity = Math.min(1, smoothIntensity + smoothTrail);
const alpha = 0.04 + finalIntensity * 0.55;
if (finalIntensity > 0.01) {
ctx.fillStyle = `rgba(${gR}, ${gG}, ${gB}, ${alpha.toFixed(3)})`;
ctx.shadowColor = `rgba(${gR}, ${gG}, ${gB}, ${(finalIntensity * 0.6).toFixed(3)})`;
ctx.shadowBlur = finalIntensity * 12;
} else {
ctx.fillStyle = "rgba(255, 255, 255, 0.04)";
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
}
roundRect(ctx, x, y, cellSize, cellSize, cornerRadius);
ctx.fill();
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
if (finalIntensity > 0.1) {
ctx.strokeStyle = `rgba(${gR}, ${gG}, ${gB}, ${(finalIntensity * 0.3).toFixed(3)})`;
ctx.lineWidth = 0.5;
roundRect(ctx, x, y, cellSize, cellSize, cornerRadius);
ctx.stroke();
}
}
}
animId = requestAnimationFrame(draw);
}
draw();
window.addEventListener("resize", handleResize);
});
onDestroy(() => {
cancelAnimationFrame(animId);
clearTimeout(resizeTimer);
window.removeEventListener("resize", handleResize);
});
</script>
<div class="grid-demo">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="grid-wrapper"
bind:this={wrapperEl}
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
on:touchmove={handleTouchMove}
on:touchend={handleTouchEnd}
>
<canvas bind:this={canvasEl} class="grid-canvas" />
<div class="vignette" />
</div>
<div class="label-overlay">
<h1 class="grid-title">Interactive Grid</h1>
<p class="grid-subtitle">Move your mouse to illuminate nearby cells</p>
</div>
</div>
<style>
.grid-demo {
width: 100vw;
height: 100vh;
background: #0a0a0a;
display: grid;
place-items: center;
position: relative;
}
.grid-wrapper {
position: absolute;
inset: 0;
overflow: hidden;
}
.grid-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 60% 60% at 50% 50%, transparent 20%, #0a0a0a 75%);
pointer-events: none;
}
.label-overlay {
position: absolute;
z-index: 10;
text-align: center;
pointer-events: none;
}
.grid-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #d1fae5 0%, #34d399 50%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 0.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.grid-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
}
</style>Interactive Grid Pattern
A canvas-based grid that responds to mouse movement in real time. Cells near the cursor illuminate with a smooth distance-based brightness falloff, creating a spotlight-like reveal effect.
How it works
- A full-screen HTML Canvas draws a grid of rounded rectangles
- On every
mousemoveevent, the cursor position is tracked - Each cell’s brightness is calculated based on its Euclidean distance from the cursor
requestAnimationFrameensures smooth 60fps rendering with lerped transitions
Customization
CELL_SIZEandGAPcontrol grid densityRADIUSsets the illumination falloff rangeBASE_COLORandGLOW_COLORdefine the dim and lit cell colors- The brightness curve uses an inverse-square falloff for natural feel
When to use it
- Interactive hero backgrounds
- Mouse-reactive ambient effects
- Dashboard backgrounds with depth
- Portfolio sites and creative showcases