Interactive Pixel Grid
Documentation
Webflow
Code
Setup: External Scripts
HTML
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>Step 1: Add HTML
HTML
<div
class="bg"
data-grid=""
data-grid-size-desktop="20"
data-grid-size-mobile="8"
data-grid-background="#FFFFFF"
data-grid-border-size="2"
data-grid-border-color="rgba(0, 0, 0, 0.2)"
data-grid-colors="[#C5D4FF, #B7B0FF, #FF5FCE, #4136FF, #FFF751, #87FEFF, #C4FF3F]"
></div>Step 2: Add CSS
CSS
.grid-bg {
position: absolute;
inset: 0%;
z-index: 0;
}Step 2: Add Javascript
Step 3: Add Javascript
Javascript
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
}
}
function initGrid(el) {
// Define default values that are used if no attributes other than [data-grid] are used
const defaults = {
gridBackground: "#FFFFFF",
gridSizeDesktop: 20,
gridSizeMobile: 8,
gridBorderSize: 2,
gridBorderColor: "rgba(0, 0, 0, 0.2)",
gridColors: ["#C5D4FF", "#B7B0FF", "#FF5FCE", "#4136FF", "#FFF751", "#87FEFF", "#C4FF3F"]
};
const gridBackground = el.getAttribute("data-grid-background") || defaults.gridBackground;
const gridSizeDesktop = parseInt(el.getAttribute("data-grid-size-desktop")) || defaults.gridSizeDesktop;
const gridSizeMobile = parseInt(el.getAttribute("data-grid-size-mobile")) || defaults.gridSizeMobile;
const gridBorderSize = parseFloat(el.getAttribute("data-grid-border-size")) || defaults.gridBorderSize;
const gridBorderColor = el.getAttribute("data-grid-border-color") || defaults.gridBorderColor;
// Parse grid colors so you can use HEX or RGBA values in the attribute
let gridColors = defaults.gridColors;
const attrColors = el.getAttribute("data-grid-colors");
if (attrColors) {
try {
gridColors = JSON.parse(attrColors);
} catch (e) {
try {
gridColors = JSON.parse(attrColors.replace(/'/g, '"'));
} catch (e2) {
gridColors = defaults.gridColors;
}
}
}
el.style.backgroundColor = gridBackground;
const canvas = document.createElement("canvas");
el.appendChild(canvas);
const ctx = canvas.getContext("2d");
let cols, rows, squareSize, blocks, lastHoveredIndex = null;
// Generate the actual grid
function setupGrid() {
canvas.width = el.offsetWidth;
canvas.height = el.offsetHeight;
cols = (window.innerWidth < 768) ? gridSizeMobile : gridSizeDesktop;
squareSize = canvas.width / cols;
rows = Math.ceil(canvas.height / squareSize);
blocks = [];
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
blocks.push({ x: x * squareSize, y: y * squareSize, color: "white", alpha: 0 });
}
}
}
// Draw the squares
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
blocks.forEach(block => {
ctx.fillStyle = block.color;
ctx.globalAlpha = block.alpha;
ctx.fillRect(block.x, block.y, squareSize, squareSize);
ctx.globalAlpha = 1;
ctx.strokeStyle = gridBorderColor;
ctx.lineWidth = gridBorderSize;
ctx.strokeRect(block.x, block.y, squareSize, squareSize);
});
requestAnimationFrame(draw);
}
// Define how long it takes for blocks to fade out
function fadeOut(block) {
gsap.to(block, { alpha: 0, duration: 2, delay: 0.5 });
}
function supportsTouch() {
return "ontouchstart" in window || navigator.maxTouchPoints;
}
// Init mousemove listener if we're NOT on a touchscreen
if (!supportsTouch()) {
canvas.addEventListener("mousemove", (event) => {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const hoveredIndex = blocks.findIndex(block =>
mouseX >= block.x &&
mouseX < block.x + squareSize &&
mouseY >= block.y &&
mouseY < block.y + squareSize
);
if (hoveredIndex !== -1 && hoveredIndex !== lastHoveredIndex) {
const block = blocks[hoveredIndex];
block.color = gridColors[Math.floor(Math.random() * gridColors.length)];
// Define duration of fade in animation
gsap.to(block, { alpha: 1, duration: 0.1, overwrite:true });
// Start fade out
fadeOut(block);
lastHoveredIndex = hoveredIndex;
}
});
}
window.addEventListener("resize", debounce(setupGrid, 200));
setupGrid();
draw();
}
function initGrids() {
document.querySelectorAll("[data-grid]").forEach(el => initGrid(el));
}
// Initialize Interactive Pixel Grid
document.addEventListener("DOMContentLoaded", () => {
initGrids();
});Implementation
This script is setup to be as flexible as possible. In essence, all you need is a div with [data-grid] attribute. All of the attributes (also listed below) are optional overrides of the default values listed in the JavaScript. So imagine you want to have the same grid all over your website, just change the default values, and you don't have to include any extra variables on your data-grid elements.
Default options:
const defaults = {
gridBackground: "#FFFFFF", // Background color of the canvas
gridSizeDesktop: 20, // Amount of blocks in a row for desktop
gridSizeMobile: 8, // Amount of blocks for screens < 768px
gridBorderSize: 2, // Stroke width of the blocks
gridBorderColor: "rgba(0, 0, 0, 0.2)", // Stroke color
// Aray with default color options for hover
gridColors: ["#C5D4FF", "#B7B0FF", "#FF5FCE", "#4136FF", "#FFF751", "#87FEFF", "#C4FF3F"]
};Available HTML attributes:
data-grid="" → The main selector, not optional
data-grid-size-desktop="20" → Controls the amount of blocks that will fit in a row.
data-grid-size-mobile="8" → Same as above, but for when the screen is smaller than 768px.
data-grid-background="#FFFFFF" → Set the background color of the canvas, can be 'transparent' too.
data-grid-border-size="2" → Set the stroke width of the blocks.
data-grid-border-color="rgba(0, 0, 0, 0.2)" → Set the color of the stroke on all the blocks.
data-grid-colors="[#C5D4FF, #B7B0FF, #FF5FCE, #4136FF, #FFF751, #87FEFF, #C4FF3F]" → An array of colors that are randomized on hover. This can be rgba values too, just wrap each color in between ' ' like so: ['rgba(0,0,0,0.1)', 'rgba(0,0,0,0.2)'].
Resource details
Published
February 28, 2025
Category
Hover Interactions
Popularity
1.2K visitors
Need help?
Join Slack