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.

backgrounds/mesh-spotlight.astro
---
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>