Interactive Dot Mesh
A tiled dot grid drawn with a single repeating gradient. A pointer-tracked radial mask makes the mesh glow only around the cursor — one element, no canvas.
- radial-gradient
- mask
- background-image tiling
Source
A single self-contained .astro component. It uses Prism's design tokens (--spectrum-*, --ink…) with sensible fallbacks, so it renders even outside this site.
---
export const meta = {
title: "Interactive Dot Mesh",
tags: ["background", "pointer", "mask"],
description:
"A tiled dot grid drawn with a single repeating gradient. A pointer-tracked radial mask makes the mesh glow only around the cursor — one element, no canvas.",
tech: ["radial-gradient", "mask", "background-image tiling"],
interactive: true,
height: 440,
};
---
<section class="mesh" data-mesh>
<div class="mesh__dots" aria-hidden="true"></div>
<div class="mesh__content">
<h2>Move your cursor</h2>
<p>The grid is a single tiled gradient; the light is a mask that follows you.</p>
</div>
</section>
<style>
.mesh {
--mx: 50%; --my: 50%;
position: relative; min-height: 440px; display: grid; place-items: center;
overflow: hidden; background: var(--bg, #0a0b12); color: var(--ink, #fff); text-align: center;
padding: 3rem 1.5rem;
}
.mesh__dots {
position: absolute; inset: 0;
background-image: radial-gradient(circle at center, color-mix(in oklab, var(--spectrum-3, #45d3e8) 90%, transparent) 1.5px, transparent 1.6px);
background-size: 26px 26px;
-webkit-mask-image: radial-gradient(18rem 18rem at var(--mx) var(--my), #000 0%, transparent 70%);
mask-image: radial-gradient(18rem 18rem at var(--mx) var(--my), #000 0%, transparent 70%);
transition: opacity 0.4s ease;
}
.mesh::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(24rem 24rem at var(--mx) var(--my), color-mix(in oklab, var(--spectrum-1,#8b5cf6) 18%, transparent), transparent 65%);
pointer-events: none;
}
.mesh__content { position: relative; z-index: 1; max-width: 32rem; display: grid; gap: 0.5rem; }
.mesh h2 { margin: 0; font-size: clamp(1.8rem, 1.2rem + 2.4vw, 3rem); letter-spacing: -0.02em; }
.mesh p { margin: 0; color: var(--ink-muted, #aeb0c0); }
</style>
<script>
(function () {
function bind() {
document.querySelectorAll<HTMLElement>("[data-mesh]").forEach((el) => {
if (el.dataset.bound) return;
el.dataset.bound = "1";
el.addEventListener("pointermove", (e) => {
const r = el.getBoundingClientRect();
el.style.setProperty("--mx", `${((e.clientX - r.left) / r.width) * 100}%`);
el.style.setProperty("--my", `${((e.clientY - r.top) / r.height) * 100}%`);
});
});
}
document.addEventListener("astro:page-load", bind);
if (document.readyState !== "loading") bind();
})();
</script>