Interactive spider grid effect

A dot grid that reacts to your cursor. Dots grow, glow, and connect with lines as you move.

Add it to any element with a single data attribute.

Fully customizable via data attributes.

1. CDN Scripts

Add these to your <head> or before closing </body> tag:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollTrigger.min.js"></script>

2. Required CSS

[data-spider-grid] {
  position: relative;
  overflow: hidden;
}

[data-spider-grid] canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
}

[data-spider-grid] > *:not(canvas) {
  position: relative;
  z-index: 1;
}

3. JavaScript

gsap.registerPlugin(ScrollTrigger);

document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll("[data-spider-grid]").forEach((el) => {
    // --- Read data attributes with defaults ---
    const dotColor    = el.dataset.dotColor    || "#e8782a";
    const lineColorRGB= el.dataset.lineColor   || "200, 180, 160";
    const gridSpacing = parseInt(el.dataset.gridSpacing)  || 60;
    const dotBase     = parseFloat(el.dataset.dotBaseSize) || 2;
    const dotMax      = parseFloat(el.dataset.dotMaxSize)  || 16;
    const effectR     = parseInt(el.dataset.effectRadius)  || 200;
    const lineR       = parseInt(el.dataset.lineRadius)    || 180;

    // --- Parse dot color to RGB ---
    const tmp = document.createElement("div");
    tmp.style.color = dotColor;
    document.body.appendChild(tmp);
    const parsed = getComputedStyle(tmp).color;
    document.body.removeChild(tmp);
    const [rBase, gBase, bBase] = parsed.match(/\d+/g).map(Number);

    // Dim version (40% brightness)
    const rDim = Math.round(rBase * 0.5);
    const gDim = Math.round(gBase * 0.5);
    const bDim = Math.round(bBase * 0.5);

    // --- Create canvas ---
    const canvas = document.createElement("canvas");
    el.insertBefore(canvas, el.firstChild);
    const ctx = canvas.getContext("2d");

    let W, H, dots = [];
    const mouse = { x: -9999, y: -9999 };

    function resize() {
      const rect = el.getBoundingClientRect();
      W = canvas.width  = rect.width;
      H = canvas.height = rect.height;
      buildGrid();
    }

    function buildGrid() {
      dots = [];
      const cols = Math.ceil(W / gridSpacing) + 1;
      const rows = Math.ceil(H / gridSpacing) + 1;
      const ox = (W - (cols - 1) * gridSpacing) / 2;
      const oy = (H - (rows - 1) * gridSpacing) / 2;
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          dots.push({ x: ox + c * gridSpacing, y: oy + r * gridSpacing });
        }
      }
    }

    // --- Mouse tracking (relative to element) ---
    el.style.pointerEvents = "auto";
    canvas.style.pointerEvents = "none";

    el.addEventListener("mousemove", (e) => {
      const rect = el.getBoundingClientRect();
      mouse.x = e.clientX - rect.left;
      mouse.y = e.clientY - rect.top;
    });

    el.addEventListener("mouseleave", () => {
      mouse.x = -9999;
      mouse.y = -9999;
    });

    el.addEventListener("touchmove", (e) => {
      const rect = el.getBoundingClientRect();
      mouse.x = e.touches[0].clientX - rect.left;
      mouse.y = e.touches[0].clientY - rect.top;
    }, { passive: true });

    el.addEventListener("touchend", () => {
      mouse.x = -9999;
      mouse.y = -9999;
    });

    // --- Animation ---
    let active = false;

    function animate() {
      if (!active) return;
      ctx.clearRect(0, 0, W, H);

      // Connection lines
      for (const dot of dots) {
        const dx = dot.x - mouse.x;
        const dy = dot.y - mouse.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < lineR) {
          const a = (1 - dist / lineR) * 0.5;
          ctx.beginPath();
          ctx.moveTo(mouse.x, mouse.y);
          ctx.lineTo(dot.x, dot.y);
          ctx.strokeStyle = `rgba(${lineColorRGB}, ${a})`;
          ctx.lineWidth = 1;
          ctx.stroke();
        }
      }

      // Dots
      for (const dot of dots) {
        const dx = dot.x - mouse.x;
        const dy = dot.y - mouse.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        const t = Math.max(0, 1 - dist / effectR);
        const size = dotBase + (dotMax - dotBase) * t;

        const r = Math.round(rDim + (rBase - rDim) * t);
        const g = Math.round(gDim + (gBase - gDim) * t);
        const b = Math.round(bDim + (bBase - bDim) * t);
        const alpha = 0.55 + 0.45 * t;

        ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
        const half = size / 2;

        if (size < 4) {
          ctx.beginPath();
          ctx.arc(dot.x, dot.y, half, 0, Math.PI * 2);
          ctx.fill();
        } else {
          ctx.beginPath();
          ctx.roundRect(dot.x - half, dot.y - half, size, size, 3);
          ctx.fill();
        }
      }

      // Cursor glow
      if (mouse.x > -9000) {
        const grad = ctx.createRadialGradient(
          mouse.x, mouse.y, 0, mouse.x, mouse.y, 8
        );
        grad.addColorStop(0, "rgba(255,255,255,0.9)");
        grad.addColorStop(1, "rgba(255,255,255,0)");
        ctx.beginPath();
        ctx.arc(mouse.x, mouse.y, 8, 0, Math.PI * 2);
        ctx.fillStyle = grad;
        ctx.fill();
      }

      requestAnimationFrame(animate);
    }

    // --- ScrollTrigger activation ---
    resize();
    window.addEventListener("resize", resize);

    ScrollTrigger.create({
      trigger: el,
      start: "top bottom",
      end: "bottom top",
      onEnter:      () => { active = true; animate(); },
      onEnterBack:  () => { active = true; animate(); },
      onLeave:      () => { active = false; },
      onLeaveBack:  () => { active = false; },
    });
  });
});

4. HTML Usage

<!-- Default orange dots -->
<section data-spider-grid>
  <h1>Your content here</h1>
</section>

<!-- Cyan dots, wider spacing -->
<section
  data-spider-grid
  data-dot-color="#22d3ee"
  data-grid-spacing="80"
  data-effect-radius="250"
>
  <p>Your content here</p>
</section>

<!-- Purple dots, smaller grid -->
<section
  data-spider-grid
  data-dot-color="#818cf8"
  data-line-color="140, 130, 255"
  data-grid-spacing="50"
  data-dot-max-size="12"
>
  <h2>Your content here</h2>
</section>

5. Data Attributes Reference

Attribute Default Description
data-spider-grid required Enables the effect on any element
data-dot-color #e8782a Active dot color (any CSS color)
data-line-color 200, 180, 160 Connection line color as R, G, B values
data-grid-spacing 60 Distance between dots in pixels
data-dot-base-size 2 Size of static dots in pixels
data-dot-max-size 16 Max size of active dots in pixels
data-effect-radius 200 Radius of the dot grow effect
data-line-radius 180 Radius of the connection lines

6. Webstudio Integration

Add the CSS to your project styles. Paste the JavaScript into a custom code embed or site-level script. Then add data-spider-grid and any optional data attributes to any element via the Settings panel.

<!-- Webstudio: Add as Custom Code embed -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollTrigger.min.js"></script>
<script>
  // Paste the full JavaScript from Step 3 here
</script>