More Animations Snippets
Animated Grid Background — Free HTML CSS JS Hero Snippet
Animated Grid Background · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Animated Grid Background — Panning Grid with Cursor Spotlight

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
- 1Paste HTML, CSS, and JSA hero renders with a subtly panning grid and a glowing spotlight.
- 2Move your cursorThe spotlight eases toward the pointer, revealing the grid beneath.
- 3Stop movingThe light drifts on its own along a slow looping path.
- 4Note the vignetteThe grid fades into the page toward the corners.
- 5Resize the gridChange background-size for denser or sparser lines.
- 6Tune the followAdjust the lerp factor and spotlight size and color.
Real-world uses
Common Use Cases
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
<!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><!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>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>
</>
);
}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>
</>
);
}<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>// @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 });
}
}