Dot Pattern — Free HTML CSS JS Dotted Background Snippet

Dot Pattern · Heroes · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

One-element dot grid
A tiled radial-gradient, no image.
Edge-fade mask
Dots dissolve into the page.
Cursor spotlight
A masked accent layer follows the pointer.
Aligned bright dots
Shared tiling lines up the two grids.
CSS-variable reveal
--x/--y move the mask cheaply.
No per-frame JS
The handler only sets two properties.
Click-through content
Dots light under the headline.
Scales to any size
Masking needs no extra work.

About this UI Snippet

Dot Pattern — Dotted Backdrop With a Cursor Spotlight

Screenshot of the Dot Pattern snippet rendered live

The dot pattern is the understated technical backdrop on infrastructure and developer sites: a faint grid of dots, calm at rest, with a soft glow that follows your cursor and lights up brighter, colored dots in its radius. This snippet builds the whole thing with pure CSS backgrounds and masks, plus two lines of JavaScript to move the spotlight — no canvas, no images.

The dot grid in one element

The base dots are a single radial-gradient — a 1px white dot over transparent — tiled across the hero with background-size: 22px 22px. That repeats the dot on a regular grid, giving graph-paper dots from one background declaration. A mask-image radial gradient then fades the dots toward the edges so the pattern dissolves into the page rather than ending at a hard border, keeping focus on the center.

The cursor spotlight

The interactive glow is a second dot layer, .dp-spot, identical in tiling but in a brighter accent color. It is revealed through a mask-image radial gradient whose center is driven by two CSS custom properties, --x and --y. A pointermove handler writes the cursor position into those variables, so the masked circle — and therefore the brighter dots — follows the pointer. Outside the circle the layer is masked away, so you only see the accent dots within the spotlight. The layer fades in on hover so the hero is monochrome and calm until you interact.

Two layers, one illusion

The effect of "dots lighting up under the cursor" is really two stacked dot grids: the faint base always visible, and the bright accent grid revealed only inside the moving mask. Because both share the same 22px tiling, the bright dots line up exactly over the faint ones, so it reads as the same dots brightening rather than a separate layer. This alignment is what sells it.

Cheap and smooth

There is no per-frame JavaScript — the handler only sets two CSS variables, and the browser composites the masked gradient move on the GPU. So the spotlight tracks the cursor smoothly with negligible cost, even on a large hero. The content sits above with pointer-events: none (except where you need clicks), so moving over the headline still lights the dots beneath.

Why masks beat redrawing

Some implementations redraw dots on a canvas each frame to brighten those near the cursor. Masking a pre-tiled gradient is far cheaper and pixel-perfect: the dots never shift, and the only thing moving is a mask center, which is a single composited property. It also scales to any hero size without more work.

Customizing it

Change background-size for a denser or sparser grid, the dot color and size, the spotlight radius (the 200px circle in the mask), or the edge-fade mask shape. Recolor the accent dots to your brand. Pair it with an aurora text headline or a particle network for a layered, modern hero.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA faint dotted grid fills the hero behind the text.
  2. 2
    Move your cursorA glow follows the pointer and brightens nearby dots.
  3. 3
    Note the edge fadeThe dots dissolve toward the edges of the hero.
  4. 4
    Hover the headlineThe dots still light up beneath the text.
  5. 5
    Resize the gridChange background-size for denser or sparser dots.
  6. 6
    Recolor the spotlightEdit the accent dot color and radius.

Real-world uses

Common Use Cases

Dev-tool heroes
Backdrop for an aurora text headline.
Infrastructure sites
Pair with a particle network elsewhere.
Docs landing pages
Frame a floating pill nav above it.
SaaS sections
A calm surface behind a feature tabs showcase.
Product launches
Set the stage for an animated gradient CTA.
CSS mask demos
A reference for cursor-spotlit patterns.

Got questions?

Frequently Asked Questions

A single radial-gradient paints a 1px dot over transparent, tiled across the element with background-size: 22px 22px. That repeats the dot on a regular grid from one background declaration. A mask-image radial gradient then fades the dots toward the edges so the pattern dissolves into the page instead of stopping at a hard border.

A second dot layer in a brighter accent color is revealed through a mask-image radial gradient centered on the --x and --y CSS variables, which a pointermove handler updates to the cursor position. Only the dots inside the masked circle show, and because both layers share the same 22px tiling, the bright dots align exactly over the faint ones, so it reads as the same dots brightening.

Masking a pre-tiled gradient is cheaper and pixel-perfect: the dots never move, and the only thing changing is a mask center, a single composited property. A canvas approach would redraw dots every frame. The mask method scales to any hero size with no extra work and has negligible runtime cost.

Yes. There is no per-frame JavaScript — the pointermove handler only writes two CSS variables, and the browser composites the masked gradient move on the GPU. So the glow tracks the cursor smoothly even on a large hero, and the content above uses pointer-events: none so it still lights the dots beneath the headline.

Keep the dot grid and edge fade as static CSS. Attach a pointermove handler that sets the --x and --y inline style on the spotlight layer via a ref. No state is needed since the move only updates CSS variables. The masks port directly; in Tailwind use arbitrary bg-[radial-gradient(...)] and mask-image values with the inline variables.

Dot Pattern — 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>Dot Pattern</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#06060e;color:#fff}
    
    .dp-hero{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;text-align:center}
    
    .dp-dots{position:absolute;inset:0;background-image:radial-gradient(rgba(255,255,255,.18) 1px,transparent 1px);background-size:22px 22px;
      -webkit-mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%);
      mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%)}
    
    /* A coloured glow that reveals brighter dots under the cursor by overlaying a
       second masked dot layer through the spotlight. */
    .dp-spot{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(rgba(129,140,248,.9) 1px,transparent 1px);background-size:22px 22px;opacity:0;transition:opacity .3s;
      -webkit-mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%);
      mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%)}
    .dp-hero:hover .dp-spot{opacity:1}
    
    .dp-content{position:relative;z-index:1;padding:0 20px;max-width:600px;pointer-events:none}
    .dp-tag{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.05em;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}
    .dp-content h1{font-size:clamp(34px,7vw,64px);font-weight:900;letter-spacing:-.03em}
    .dp-content p{margin-top:14px;font-size:16px;color:#9a9ac0;line-height:1.55}
  </style>
</head>
<body>
  <section class="dp-hero" id="dpHero">
    <div class="dp-dots" aria-hidden="true"></div>
    <div class="dp-spot" id="dpSpot" aria-hidden="true"></div>
    <div class="dp-content">
      <span class="dp-tag">◇ Infrastructure</span>
      <h1>Quietly powerful</h1>
      <p>A subtle dot grid backdrop with a glow that follows your cursor — one element, pure CSS.</p>
    </div>
  </section>
  <script>
    var hero = document.getElementById('dpHero');
    var spot = document.getElementById('dpSpot');
    
    // Move the spotlight mask to the cursor via two CSS variables — no per-frame
    // JS work beyond setting properties.
    hero.addEventListener('pointermove', function (e) {
      var r = hero.getBoundingClientRect();
      spot.style.setProperty('--x', (e.clientX - r.left) + 'px');
      spot.style.setProperty('--y', (e.clientY - r.top) + 'px');
    });
  </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>Dot Pattern</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    * {
      box-sizing:border-box;margin:0;padding:0
    }

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

    .dp-hero:hover .dp-spot {
      opacity:1
    }

    .dp-content h1 {
      font-size:clamp(34px,7vw,64px);font-weight:900;letter-spacing:-.03em
    }

    .dp-content p {
      margin-top:14px;font-size:16px;color:#9a9ac0;line-height:1.55
    }
  </style>
</head>
<body>
  <section class="dp-hero relative min-h-screen overflow-hidden flex items-center justify-center text-center" id="dpHero">
    <div class="absolute inset-0 [background-image:radial-gradient(rgba(255,255,255,.18)_1px,transparent_1px)] bg-[size:22px_22px] [-webkit-mask-image:radial-gradient(ellipse_at_center,#000_30%,transparent_75%)] [mask-image:radial-gradient(ellipse_at_center,#000_30%,transparent_75%)]" aria-hidden="true"></div>
    <div class="dp-spot absolute inset-0 pointer-events-none [background-image:radial-gradient(rgba(129,140,248,.9)_1px,transparent_1px)] bg-[size:22px_22px] opacity-0 [transition:opacity_.3s] [-webkit-mask-image:radial-gradient(200px_circle_at_var(--x,-200px)_var(--y,-200px),#000_0%,transparent_70%)] [mask-image:radial-gradient(200px_circle_at_var(--x,-200px)_var(--y,-200px),#000_0%,transparent_70%)]" id="dpSpot" aria-hidden="true"></div>
    <div class="dp-content relative z-[1] py-0 px-5 max-w-[600px] pointer-events-none">
      <span class="inline-block text-xs font-bold tracking-[.05em] 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>Quietly powerful</h1>
      <p>A subtle dot grid backdrop with a glow that follows your cursor — one element, pure CSS.</p>
    </div>
  </section>
  <script>
    var hero = document.getElementById('dpHero');
    var spot = document.getElementById('dpSpot');
    
    // Move the spotlight mask to the cursor via two CSS variables — no per-frame
    // JS work beyond setting properties.
    hero.addEventListener('pointermove', function (e) {
      var r = hero.getBoundingClientRect();
      spot.style.setProperty('--x', (e.clientX - r.left) + 'px');
      spot.style.setProperty('--y', (e.clientY - r.top) + 'px');
    });
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

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

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

.dp-dots{position:absolute;inset:0;background-image:radial-gradient(rgba(255,255,255,.18) 1px,transparent 1px);background-size:22px 22px;
  -webkit-mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%);
  mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%)}

/* A coloured glow that reveals brighter dots under the cursor by overlaying a
   second masked dot layer through the spotlight. */
.dp-spot{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(rgba(129,140,248,.9) 1px,transparent 1px);background-size:22px 22px;opacity:0;transition:opacity .3s;
  -webkit-mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%);
  mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%)}
.dp-hero:hover .dp-spot{opacity:1}

.dp-content{position:relative;z-index:1;padding:0 20px;max-width:600px;pointer-events:none}
.dp-tag{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.05em;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}
.dp-content h1{font-size:clamp(34px,7vw,64px);font-weight:900;letter-spacing:-.03em}
.dp-content p{margin-top:14px;font-size:16px;color:#9a9ac0;line-height:1.55}
`;

export default function DotPattern() {
  // 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('dpHero');
    var spot = document.getElementById('dpSpot');
    
    // Move the spotlight mask to the cursor via two CSS variables — no per-frame
    // JS work beyond setting properties.
    hero.addEventListener('pointermove', function (e) {
      var r = hero.getBoundingClientRect();
      spot.style.setProperty('--x', (e.clientX - r.left) + 'px');
      spot.style.setProperty('--y', (e.clientY - r.top) + 'px');
    });
    // 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="dp-hero" id="dpHero">
        <div className="dp-dots" aria-hidden="true"></div>
        <div className="dp-spot" id="dpSpot" aria-hidden="true"></div>
        <div className="dp-content">
          <span className="dp-tag">◇ Infrastructure</span>
          <h1>Quietly powerful</h1>
          <p>A subtle dot grid backdrop with a glow that follows your cursor — one element, pure CSS.</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 DotPattern() {
  // 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('dpHero');
    var spot = document.getElementById('dpSpot');
    
    // Move the spotlight mask to the cursor via two CSS variables — no per-frame
    // JS work beyond setting properties.
    hero.addEventListener('pointermove', function (e) {
      var r = hero.getBoundingClientRect();
      spot.style.setProperty('--x', (e.clientX - r.left) + 'px');
      spot.style.setProperty('--y', (e.clientY - r.top) + 'px');
    });
    // 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>{`
* {
  box-sizing:border-box;margin:0;padding:0
}

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

.dp-hero:hover .dp-spot {
  opacity:1
}

.dp-content h1 {
  font-size:clamp(34px,7vw,64px);font-weight:900;letter-spacing:-.03em
}

.dp-content p {
  margin-top:14px;font-size:16px;color:#9a9ac0;line-height:1.55
}
      `}</style>
      <section className="dp-hero relative min-h-screen overflow-hidden flex items-center justify-center text-center" id="dpHero">
        <div className="absolute inset-0 [background-image:radial-gradient(rgba(255,255,255,.18)_1px,transparent_1px)] bg-[size:22px_22px] [-webkit-mask-image:radial-gradient(ellipse_at_center,#000_30%,transparent_75%)] [mask-image:radial-gradient(ellipse_at_center,#000_30%,transparent_75%)]" aria-hidden="true"></div>
        <div className="dp-spot absolute inset-0 pointer-events-none [background-image:radial-gradient(rgba(129,140,248,.9)_1px,transparent_1px)] bg-[size:22px_22px] opacity-0 [transition:opacity_.3s] [-webkit-mask-image:radial-gradient(200px_circle_at_var(--x,-200px)_var(--y,-200px),#000_0%,transparent_70%)] [mask-image:radial-gradient(200px_circle_at_var(--x,-200px)_var(--y,-200px),#000_0%,transparent_70%)]" id="dpSpot" aria-hidden="true"></div>
        <div className="dp-content relative z-[1] py-0 px-5 max-w-[600px] pointer-events-none">
          <span className="inline-block text-xs font-bold tracking-[.05em] 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>Quietly powerful</h1>
          <p>A subtle dot grid backdrop with a glow that follows your cursor — one element, pure CSS.</p>
        </div>
      </section>
    </>
  );
}
Vue
<template>
  <section class="dp-hero" id="dpHero">
    <div class="dp-dots" aria-hidden="true"></div>
    <div class="dp-spot" id="dpSpot" aria-hidden="true"></div>
    <div class="dp-content">
      <span class="dp-tag">◇ Infrastructure</span>
      <h1>Quietly powerful</h1>
      <p>A subtle dot grid backdrop with a glow that follows your cursor — one element, pure CSS.</p>
    </div>
  </section>
</template>

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

let hero;
let spot;

onMounted(() => {
  hero = document.getElementById('dpHero');
  spot = document.getElementById('dpSpot');
  // Move the spotlight mask to the cursor via two CSS variables — no per-frame
  // JS work beyond setting properties.
  hero.addEventListener('pointermove', function (e) {
    var r = hero.getBoundingClientRect();
    spot.style.setProperty('--x', (e.clientX - r.left) + 'px');
    spot.style.setProperty('--y', (e.clientY - r.top) + 'px');
  });
});
</script>

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

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

.dp-dots{position:absolute;inset:0;background-image:radial-gradient(rgba(255,255,255,.18) 1px,transparent 1px);background-size:22px 22px;
  -webkit-mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%);
  mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%)}

/* A coloured glow that reveals brighter dots under the cursor by overlaying a
   second masked dot layer through the spotlight. */
.dp-spot{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(rgba(129,140,248,.9) 1px,transparent 1px);background-size:22px 22px;opacity:0;transition:opacity .3s;
  -webkit-mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%);
  mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%)}
.dp-hero:hover .dp-spot{opacity:1}

.dp-content{position:relative;z-index:1;padding:0 20px;max-width:600px;pointer-events:none}
.dp-tag{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.05em;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}
.dp-content h1{font-size:clamp(34px,7vw,64px);font-weight:900;letter-spacing:-.03em}
.dp-content p{margin-top:14px;font-size:16px;color:#9a9ac0;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-dot-pattern',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <section class="dp-hero" id="dpHero">
      <div class="dp-dots" aria-hidden="true"></div>
      <div class="dp-spot" id="dpSpot" aria-hidden="true"></div>
      <div class="dp-content">
        <span class="dp-tag">◇ Infrastructure</span>
        <h1>Quietly powerful</h1>
        <p>A subtle dot grid backdrop with a glow that follows your cursor — one element, pure CSS.</p>
      </div>
    </section>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#06060e;color:#fff}
    
    .dp-hero{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;text-align:center}
    
    .dp-dots{position:absolute;inset:0;background-image:radial-gradient(rgba(255,255,255,.18) 1px,transparent 1px);background-size:22px 22px;
      -webkit-mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%);
      mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%)}
    
    /* A coloured glow that reveals brighter dots under the cursor by overlaying a
       second masked dot layer through the spotlight. */
    .dp-spot{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(rgba(129,140,248,.9) 1px,transparent 1px);background-size:22px 22px;opacity:0;transition:opacity .3s;
      -webkit-mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%);
      mask-image:radial-gradient(200px circle at var(--x,-200px) var(--y,-200px),#000 0%,transparent 70%)}
    .dp-hero:hover .dp-spot{opacity:1}
    
    .dp-content{position:relative;z-index:1;padding:0 20px;max-width:600px;pointer-events:none}
    .dp-tag{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.05em;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}
    .dp-content h1{font-size:clamp(34px,7vw,64px);font-weight:900;letter-spacing:-.03em}
    .dp-content p{margin-top:14px;font-size:16px;color:#9a9ac0;line-height:1.55}
  `]
})
export class DotPatternComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var hero = document.getElementById('dpHero');
    var spot = document.getElementById('dpSpot');
    
    // Move the spotlight mask to the cursor via two CSS variables — no per-frame
    // JS work beyond setting properties.
    hero.addEventListener('pointermove', function (e) {
      var r = hero.getBoundingClientRect();
      spot.style.setProperty('--x', (e.clientX - r.left) + 'px');
      spot.style.setProperty('--y', (e.clientY - r.top) + 'px');
    });
  }
}