Animated Grid Background — Free HTML CSS JS Hero Snippet

Animated Grid Background · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

CSS-only grid
Two linear-gradients tiled — no SVG or image.
Seamless pan
background-position shifts one full cell and loops.
Cursor spotlight
A blurred radial light follows the pointer.
Lerp smoothing
The light eases toward its target each frame.
Idle Lissajous drift
It wanders on a smooth curve when idle.
Single unified loop
One rAF handles cursor and idle modes.
Vignette fade
A radial mask dissolves the grid into the page.
Non-blocking light
pointer-events none keeps the hero interactive.

About this UI Snippet

Animated Grid Background — Panning Grid with Cursor Spotlight

Screenshot of the Animated Grid Background snippet rendered live

The animated grid background is the technical-but-elegant hero backdrop seen on infrastructure, dev-tool, and crypto sites: a subtle grid of lines that slowly pans, lit by a soft glowing spotlight that follows your cursor — and gently drifts on its own when the mouse is idle. This snippet builds the whole scene in plain HTML, CSS, and one small vanilla JavaScript loop, with no canvas and no images.

The grid, drawn with gradients

The grid is a pure-CSS pattern: two linear-gradient backgrounds — one for horizontal lines, one vertical — each a 1px opaque line over transparent, tiled at 46px via background-size. Layering the two gives a crisp graph-paper grid with no SVG or image. Animating background-position by exactly one tile (46px 46px) over 18 seconds on the gbPan keyframe makes the whole grid pan diagonally and loop seamlessly, because shifting by one full cell lands on an identical pattern.

The cursor spotlight

A blurred radial-gradient circle, .gb-spot, acts as a light that reveals the grid beneath it. A pointermove listener converts the cursor position into normalized 0–1 coordinates relative to the hero, which become the spotlight's target. Because the spotlight is pointer-events: none, it never blocks interaction, and its filter: blur(20px) softens it into ambient light rather than a hard disc.

Smoothing with linear interpolation

Raw pointer coordinates are jittery, so the animation loop eases the spotlight toward its target using lerp: current += (target - current) * 0.08 each frame. This is the standard trick for buttery cursor-following — the light always chases the pointer but lags slightly, giving a fluid, weighted feel instead of snapping. The 0.08 factor controls how tight or loose that follow is.

Idle drift on a Lissajous path

The detail that makes this feel premium: when the pointer leaves or hasn't moved, the spotlight doesn't freeze. A hasPointer flag switches the target to a slowly evolving Lissajous curve — cos(t) for x and sin(t * 1.3) for y — so the light wanders in a smooth, never-repeating loop around the hero. The moment you move the mouse again, the target snaps back to the cursor and the same lerp eases it over. One loop handles both modes.

The vignette

A .gb-fade layer is a radial gradient from transparent in the center to the page background at the edges, darkening the grid toward the corners. This focuses attention on the centered content and hides the grid's hard edges, so the pattern appears to dissolve into the page rather than stopping at a border.

Customizing it

Change background-size for a denser or sparser grid, recolor the line opacity for a bolder or subtler look, resize and recolor the spotlight's radial gradient, and tune the lerp factor for a tighter or lazier cursor follow. Adjust the Lissajous frequencies for a different idle path. Pair it with text generate headline copy or a shimmer button to complete a developer-product hero.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA hero renders with a subtly panning grid and a glowing spotlight.
  2. 2
    Move your cursorThe spotlight eases toward the pointer, revealing the grid beneath.
  3. 3
    Stop movingThe light drifts on its own along a slow looping path.
  4. 4
    Note the vignetteThe grid fades into the page toward the corners.
  5. 5
    Resize the gridChange background-size for denser or sparser lines.
  6. 6
    Tune the followAdjust the lerp factor and spotlight size and color.

Real-world uses

Common Use Cases

Dev-tool heroes
Backdrop for text generate headline copy.
Infrastructure sites
Pair with a shimmer button CTA.
Crypto and fintech
A technical backdrop behind a startup hero.
Docs landing pages
Frame a floating pill nav above it.
Product launches
Set the stage for an animated gradient CTA.
Spotlight effect demos
A reference for lerp-based cursor following.

Got questions?

Frequently Asked Questions

Two layered linear-gradients — one horizontal, one vertical — each paint a 1px opaque line over transparent, tiled at 46px via background-size. That produces a crisp graph-paper grid in pure CSS. Animating background-position by exactly one 46px cell loops seamlessly because shifting by a full tile lands on an identical pattern.

The loop eases the light toward the pointer with linear interpolation: current += (target - current) * 0.08 each frame. Raw pointer coordinates are jittery, so this lerp gives a smooth, weighted follow where the light always chases but trails slightly. The 0.08 factor sets how tight or loose the follow feels.

A hasPointer flag flips on pointerleave, switching the target to a slowly evolving Lissajous curve — cos(t) for x and sin(t*1.3) for y — so the spotlight wanders in a smooth, non-repeating loop. Moving the mouse again resets the target to the cursor, and the same lerp eases it back.

A vignette layer is a radial gradient from transparent at the center to the page background color at the edges. It darkens the grid toward the corners, focusing attention on the centered content and hiding the grid's hard boundary so the pattern appears to dissolve into the page.

Keep the grid and vignette as static CSS layers. Run the rAF loop in a mount effect, storing target and current in refs so pointer moves don't trigger re-renders, and cancel the frame on unmount. Attach pointermove and pointerleave to the hero ref. In Tailwind, build the grid with a bg-[linear-gradient(...)] arbitrary value and animate background-position via a keyframe in the config.

Animated Grid Background — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Animated Grid Background</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#05050a;color:#fff}
    
    .gb-hero{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;text-align:center}
    
    .gb-grid{position:absolute;inset:-2px;background-image:linear-gradient(rgba(255,255,255,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.07) 1px,transparent 1px);background-size:46px 46px;animation:gbPan 18s linear infinite}
    @keyframes gbPan{to{background-position:46px 46px}}
    
    .gb-spot{position:absolute;width:420px;height:420px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,.5),transparent 65%);transform:translate(-50%,-50%);left:50%;top:40%;pointer-events:none;filter:blur(20px);transition:left .12s ease-out,top .12s ease-out}
    
    .gb-fade{position:absolute;inset:0;background:radial-gradient(ellipse at center,transparent 30%,#05050a 78%)}
    
    .gb-content{position:relative;z-index:1;padding:0 20px;max-width:620px}
    .gb-pill{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.04em;color:#c7d2fe;background:rgba(99,102,241,.16);border:1px solid rgba(99,102,241,.4);padding:6px 13px;border-radius:999px;margin-bottom:18px}
    .gb-content h1{font-size:clamp(30px,6.5vw,56px);font-weight:900;letter-spacing:-.03em;line-height:1.07;margin-bottom:14px}
    .gb-content p{font-size:15.5px;color:#a9a9c2;line-height:1.55}
  </style>
</head>
<body>
  <section class="gb-hero" id="gbHero">
    <div class="gb-grid" aria-hidden="true"></div>
    <div class="gb-spot" id="gbSpot" aria-hidden="true"></div>
    <div class="gb-fade" aria-hidden="true"></div>
    <div class="gb-content">
      <span class="gb-pill">◆ Infrastructure</span>
      <h1>Deploy on a grid<br>that moves with you</h1>
      <p>An animated dot-grid backdrop with a cursor spotlight — pure CSS and one tiny script.</p>
    </div>
  </section>
  <script>
    var hero = document.getElementById('gbHero');
    var spot = document.getElementById('gbSpot');
    var target = { x: 0.5, y: 0.4 }, current = { x: 0.5, y: 0.4 };
    var hasPointer = false;
    
    hero.addEventListener('pointermove', function (e) {
      var rect = hero.getBoundingClientRect();
      target.x = (e.clientX - rect.left) / rect.width;
      target.y = (e.clientY - rect.top) / rect.height;
      hasPointer = true;
    });
    hero.addEventListener('pointerleave', function () { hasPointer = false; });
    
    var t = 0;
    function loop() {
      t += 0.012;
      // When idle, the spotlight drifts on a slow Lissajous path; when the pointer
      // is active, it eases toward the cursor. Either way we lerp for smoothness.
      if (!hasPointer) {
        target.x = 0.5 + Math.cos(t) * 0.22;
        target.y = 0.42 + Math.sin(t * 1.3) * 0.18;
      }
      current.x += (target.x - current.x) * 0.08;
      current.y += (target.y - current.y) * 0.08;
      spot.style.left = (current.x * 100) + '%';
      spot.style.top = (current.y * 100) + '%';
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Animated Grid Background</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes gbPan{to{background-position:46px 46px}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#05050a;color:#fff
    }

    .gb-content h1 {
      font-size:clamp(30px,6.5vw,56px);font-weight:900;letter-spacing:-.03em;line-height:1.07;margin-bottom:14px
    }

    .gb-content p {
      font-size:15.5px;color:#a9a9c2;line-height:1.55
    }
  </style>
</head>
<body>
  <section class="relative min-h-screen overflow-hidden flex items-center justify-center text-center" id="gbHero">
    <div class="absolute -inset-0.5 [background-image:linear-gradient(rgba(255,255,255,.07)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.07)_1px,transparent_1px)] bg-[size:46px_46px] [animation:gbPan_18s_linear_infinite]" aria-hidden="true"></div>
    <div class="absolute w-[420px] h-[420px] rounded-full [background:radial-gradient(circle,rgba(99,102,241,.5),transparent_65%)] [transform:translate(-50%,-50%)] left-1/2 top-2/5 pointer-events-none [filter:blur(20px)] [transition:left_.12s_ease-out,top_.12s_ease-out]" id="gbSpot" aria-hidden="true"></div>
    <div class="absolute inset-0 [background:radial-gradient(ellipse_at_center,transparent_30%,#05050a_78%)]" aria-hidden="true"></div>
    <div class="gb-content relative z-[1] py-0 px-5 max-w-[620px]">
      <span class="inline-block text-xs font-bold tracking-[.04em] text-[#c7d2fe] bg-[rgba(99,102,241,.16)] border border-[rgba(99,102,241,.4)] py-1.5 px-[13px] rounded-[999px] mb-[18px]">◆ Infrastructure</span>
      <h1>Deploy on a grid<br>that moves with you</h1>
      <p>An animated dot-grid backdrop with a cursor spotlight — pure CSS and one tiny script.</p>
    </div>
  </section>
  <script>
    var hero = document.getElementById('gbHero');
    var spot = document.getElementById('gbSpot');
    var target = { x: 0.5, y: 0.4 }, current = { x: 0.5, y: 0.4 };
    var hasPointer = false;
    
    hero.addEventListener('pointermove', function (e) {
      var rect = hero.getBoundingClientRect();
      target.x = (e.clientX - rect.left) / rect.width;
      target.y = (e.clientY - rect.top) / rect.height;
      hasPointer = true;
    });
    hero.addEventListener('pointerleave', function () { hasPointer = false; });
    
    var t = 0;
    function loop() {
      t += 0.012;
      // When idle, the spotlight drifts on a slow Lissajous path; when the pointer
      // is active, it eases toward the cursor. Either way we lerp for smoothness.
      if (!hasPointer) {
        target.x = 0.5 + Math.cos(t) * 0.22;
        target.y = 0.42 + Math.sin(t * 1.3) * 0.18;
      }
      current.x += (target.x - current.x) * 0.08;
      current.y += (target.y - current.y) * 0.08;
      spot.style.left = (current.x * 100) + '%';
      spot.style.top = (current.y * 100) + '%';
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to AnimatedGridBackground.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#05050a;color:#fff}

.gb-hero{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;text-align:center}

.gb-grid{position:absolute;inset:-2px;background-image:linear-gradient(rgba(255,255,255,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.07) 1px,transparent 1px);background-size:46px 46px;animation:gbPan 18s linear infinite}
@keyframes gbPan{to{background-position:46px 46px}}

.gb-spot{position:absolute;width:420px;height:420px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,.5),transparent 65%);transform:translate(-50%,-50%);left:50%;top:40%;pointer-events:none;filter:blur(20px);transition:left .12s ease-out,top .12s ease-out}

.gb-fade{position:absolute;inset:0;background:radial-gradient(ellipse at center,transparent 30%,#05050a 78%)}

.gb-content{position:relative;z-index:1;padding:0 20px;max-width:620px}
.gb-pill{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.04em;color:#c7d2fe;background:rgba(99,102,241,.16);border:1px solid rgba(99,102,241,.4);padding:6px 13px;border-radius:999px;margin-bottom:18px}
.gb-content h1{font-size:clamp(30px,6.5vw,56px);font-weight:900;letter-spacing:-.03em;line-height:1.07;margin-bottom:14px}
.gb-content p{font-size:15.5px;color:#a9a9c2;line-height:1.55}
`;

export default function AnimatedGridBackground() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var hero = document.getElementById('gbHero');
    var spot = document.getElementById('gbSpot');
    var target = { x: 0.5, y: 0.4 }, current = { x: 0.5, y: 0.4 };
    var hasPointer = false;
    
    hero.addEventListener('pointermove', function (e) {
      var rect = hero.getBoundingClientRect();
      target.x = (e.clientX - rect.left) / rect.width;
      target.y = (e.clientY - rect.top) / rect.height;
      hasPointer = true;
    });
    hero.addEventListener('pointerleave', function () { hasPointer = false; });
    
    var t = 0;
    function loop() {
      t += 0.012;
      // When idle, the spotlight drifts on a slow Lissajous path; when the pointer
      // is active, it eases toward the cursor. Either way we lerp for smoothness.
      if (!hasPointer) {
        target.x = 0.5 + Math.cos(t) * 0.22;
        target.y = 0.42 + Math.sin(t * 1.3) * 0.18;
      }
      current.x += (target.x - current.x) * 0.08;
      current.y += (target.y - current.y) * 0.08;
      spot.style.left = (current.x * 100) + '%';
      spot.style.top = (current.y * 100) + '%';
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <section className="gb-hero" id="gbHero">
        <div className="gb-grid" aria-hidden="true"></div>
        <div className="gb-spot" id="gbSpot" aria-hidden="true"></div>
        <div className="gb-fade" aria-hidden="true"></div>
        <div className="gb-content">
          <span className="gb-pill">◆ Infrastructure</span>
          <h1>Deploy on a grid<br />that moves with you</h1>
          <p>An animated dot-grid backdrop with a cursor spotlight — pure CSS and one tiny script.</p>
        </div>
      </section>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function AnimatedGridBackground() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var hero = document.getElementById('gbHero');
    var spot = document.getElementById('gbSpot');
    var target = { x: 0.5, y: 0.4 }, current = { x: 0.5, y: 0.4 };
    var hasPointer = false;
    
    hero.addEventListener('pointermove', function (e) {
      var rect = hero.getBoundingClientRect();
      target.x = (e.clientX - rect.left) / rect.width;
      target.y = (e.clientY - rect.top) / rect.height;
      hasPointer = true;
    });
    hero.addEventListener('pointerleave', function () { hasPointer = false; });
    
    var t = 0;
    function loop() {
      t += 0.012;
      // When idle, the spotlight drifts on a slow Lissajous path; when the pointer
      // is active, it eases toward the cursor. Either way we lerp for smoothness.
      if (!hasPointer) {
        target.x = 0.5 + Math.cos(t) * 0.22;
        target.y = 0.42 + Math.sin(t * 1.3) * 0.18;
      }
      current.x += (target.x - current.x) * 0.08;
      current.y += (target.y - current.y) * 0.08;
      spot.style.left = (current.x * 100) + '%';
      spot.style.top = (current.y * 100) + '%';
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes gbPan{to{background-position:46px 46px}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#05050a;color:#fff
}

.gb-content h1 {
  font-size:clamp(30px,6.5vw,56px);font-weight:900;letter-spacing:-.03em;line-height:1.07;margin-bottom:14px
}

.gb-content p {
  font-size:15.5px;color:#a9a9c2;line-height:1.55
}
      `}</style>
      <section className="relative min-h-screen overflow-hidden flex items-center justify-center text-center" id="gbHero">
        <div className="absolute -inset-0.5 [background-image:linear-gradient(rgba(255,255,255,.07)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.07)_1px,transparent_1px)] bg-[size:46px_46px] [animation:gbPan_18s_linear_infinite]" aria-hidden="true"></div>
        <div className="absolute w-[420px] h-[420px] rounded-full [background:radial-gradient(circle,rgba(99,102,241,.5),transparent_65%)] [transform:translate(-50%,-50%)] left-1/2 top-2/5 pointer-events-none [filter:blur(20px)] [transition:left_.12s_ease-out,top_.12s_ease-out]" id="gbSpot" aria-hidden="true"></div>
        <div className="absolute inset-0 [background:radial-gradient(ellipse_at_center,transparent_30%,#05050a_78%)]" aria-hidden="true"></div>
        <div className="gb-content relative z-[1] py-0 px-5 max-w-[620px]">
          <span className="inline-block text-xs font-bold tracking-[.04em] text-[#c7d2fe] bg-[rgba(99,102,241,.16)] border border-[rgba(99,102,241,.4)] py-1.5 px-[13px] rounded-[999px] mb-[18px]">◆ Infrastructure</span>
          <h1>Deploy on a grid<br />that moves with you</h1>
          <p>An animated dot-grid backdrop with a cursor spotlight — pure CSS and one tiny script.</p>
        </div>
      </section>
    </>
  );
}
Vue
<template>
  <section class="gb-hero" id="gbHero">
    <div class="gb-grid" aria-hidden="true"></div>
    <div class="gb-spot" id="gbSpot" aria-hidden="true"></div>
    <div class="gb-fade" aria-hidden="true"></div>
    <div class="gb-content">
      <span class="gb-pill">◆ Infrastructure</span>
      <h1>Deploy on a grid<br>that moves with you</h1>
      <p>An animated dot-grid backdrop with a cursor spotlight — pure CSS and one tiny script.</p>
    </div>
  </section>
</template>

<script setup>
import { onMounted } from 'vue';

let hero;
let spot;
var target = { x: 0.5, y: 0.4 }, current = { x: 0.5, y: 0.4 };
var hasPointer = false;
var t = 0;
function loop() {
  t += 0.012;
  // When idle, the spotlight drifts on a slow Lissajous path; when the pointer
  // is active, it eases toward the cursor. Either way we lerp for smoothness.
  if (!hasPointer) {
    target.x = 0.5 + Math.cos(t) * 0.22;
    target.y = 0.42 + Math.sin(t * 1.3) * 0.18;
  }
  current.x += (target.x - current.x) * 0.08;
  current.y += (target.y - current.y) * 0.08;
  spot.style.left = (current.x * 100) + '%';
  spot.style.top = (current.y * 100) + '%';
  requestAnimationFrame(loop);
}

onMounted(() => {
  hero = document.getElementById('gbHero');
  spot = document.getElementById('gbSpot');
  hero.addEventListener('pointermove', function (e) {
    var rect = hero.getBoundingClientRect();
    target.x = (e.clientX - rect.left) / rect.width;
    target.y = (e.clientY - rect.top) / rect.height;
    hasPointer = true;
  });
  hero.addEventListener('pointerleave', function () { hasPointer = false; });
  requestAnimationFrame(loop);
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#05050a;color:#fff}

.gb-hero{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;text-align:center}

.gb-grid{position:absolute;inset:-2px;background-image:linear-gradient(rgba(255,255,255,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.07) 1px,transparent 1px);background-size:46px 46px;animation:gbPan 18s linear infinite}
@keyframes gbPan{to{background-position:46px 46px}}

.gb-spot{position:absolute;width:420px;height:420px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,.5),transparent 65%);transform:translate(-50%,-50%);left:50%;top:40%;pointer-events:none;filter:blur(20px);transition:left .12s ease-out,top .12s ease-out}

.gb-fade{position:absolute;inset:0;background:radial-gradient(ellipse at center,transparent 30%,#05050a 78%)}

.gb-content{position:relative;z-index:1;padding:0 20px;max-width:620px}
.gb-pill{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.04em;color:#c7d2fe;background:rgba(99,102,241,.16);border:1px solid rgba(99,102,241,.4);padding:6px 13px;border-radius:999px;margin-bottom:18px}
.gb-content h1{font-size:clamp(30px,6.5vw,56px);font-weight:900;letter-spacing:-.03em;line-height:1.07;margin-bottom:14px}
.gb-content p{font-size:15.5px;color:#a9a9c2;line-height:1.55}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-animated-grid-background',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <section class="gb-hero" id="gbHero">
      <div class="gb-grid" aria-hidden="true"></div>
      <div class="gb-spot" id="gbSpot" aria-hidden="true"></div>
      <div class="gb-fade" aria-hidden="true"></div>
      <div class="gb-content">
        <span class="gb-pill">◆ Infrastructure</span>
        <h1>Deploy on a grid<br>that moves with you</h1>
        <p>An animated dot-grid backdrop with a cursor spotlight — pure CSS and one tiny script.</p>
      </div>
    </section>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#05050a;color:#fff}
    
    .gb-hero{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;text-align:center}
    
    .gb-grid{position:absolute;inset:-2px;background-image:linear-gradient(rgba(255,255,255,.07) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.07) 1px,transparent 1px);background-size:46px 46px;animation:gbPan 18s linear infinite}
    @keyframes gbPan{to{background-position:46px 46px}}
    
    .gb-spot{position:absolute;width:420px;height:420px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,.5),transparent 65%);transform:translate(-50%,-50%);left:50%;top:40%;pointer-events:none;filter:blur(20px);transition:left .12s ease-out,top .12s ease-out}
    
    .gb-fade{position:absolute;inset:0;background:radial-gradient(ellipse at center,transparent 30%,#05050a 78%)}
    
    .gb-content{position:relative;z-index:1;padding:0 20px;max-width:620px}
    .gb-pill{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.04em;color:#c7d2fe;background:rgba(99,102,241,.16);border:1px solid rgba(99,102,241,.4);padding:6px 13px;border-radius:999px;margin-bottom:18px}
    .gb-content h1{font-size:clamp(30px,6.5vw,56px);font-weight:900;letter-spacing:-.03em;line-height:1.07;margin-bottom:14px}
    .gb-content p{font-size:15.5px;color:#a9a9c2;line-height:1.55}
  `]
})
export class AnimatedGridBackgroundComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var hero = document.getElementById('gbHero');
    var spot = document.getElementById('gbSpot');
    var target = { x: 0.5, y: 0.4 }, current = { x: 0.5, y: 0.4 };
    var hasPointer = false;
    
    hero.addEventListener('pointermove', function (e) {
      var rect = hero.getBoundingClientRect();
      target.x = (e.clientX - rect.left) / rect.width;
      target.y = (e.clientY - rect.top) / rect.height;
      hasPointer = true;
    });
    hero.addEventListener('pointerleave', function () { hasPointer = false; });
    
    var t = 0;
    function loop() {
      t += 0.012;
      // When idle, the spotlight drifts on a slow Lissajous path; when the pointer
      // is active, it eases toward the cursor. Either way we lerp for smoothness.
      if (!hasPointer) {
        target.x = 0.5 + Math.cos(t) * 0.22;
        target.y = 0.42 + Math.sin(t * 1.3) * 0.18;
      }
      current.x += (target.x - current.x) * 0.08;
      current.y += (target.y - current.y) * 0.08;
      spot.style.left = (current.x * 100) + '%';
      spot.style.top = (current.y * 100) + '%';
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { loop });
  }
}