@bl0cka.de/dots-pattern (0.0.6)
Installation
@bl0cka.de:registry=npm install @bl0cka.de/dots-pattern@0.0.6"@bl0cka.de/dots-pattern": "0.0.6"About this package
dots-pattern
An animated dot-matrix background — an organic, wavy field of dots whose sizes are driven by a small, modular, tree-shakable motion pipeline. Framework-agnostic core with zero runtime dependencies, plus optional React and Vue wrappers.
- Perfect-circle dots on a square or hex grid
- Organic motion from a built-in simplex noise field (or a cheap sine field)
- Pointer interaction — grow / shrink / repel
- Spatial fade — shrink dots to nothing toward any edge or radially (dots truly disappear, not just go transparent)
- Pluggable
Renderer(Canvas 2D today; WebGL/Pixi can drop in later) - Respects
prefers-reduced-motion, pauses when offscreen, DPR-aware
Install
npm install dots-pattern
Quick start
import { createDotsPattern } from 'dots-pattern'
import { noiseField, gradient, pointer } from 'dots-pattern/motion'
const dots = createDotsPattern(document.querySelector('#hero'), {
spacing: 24,
maxRadius: 4,
minRadius: 0, // dots can shrink all the way to nothing
color: '#4a2b47',
motion: {
mutators: [
noiseField({ waveSize: 450, speed: 30, morph: 0.12 }), // big waves drifting sideways
gradient({ axis: 'y', from: 0.35, to: 1 }), // dots grow toward the bottom
pointer({ mode: 'grow', radius: 120 }),
],
},
})
// dots.start() / dots.stop() / dots.setOptions({ color: '#111' }) / dots.destroy()
Pass a container element (a canvas is created and positioned to fill it) or an existing
<canvas>.
Motion is modular
Each animation is a Mutator applied in order every frame. Animators add to a
dot's scale; the fade envelope multiplies its mask (0 → invisible). Add your
own without touching the library:
import type { Mutator } from 'dots-pattern'
const ripple: Mutator = {
id: 'ripple',
apply(dot, state, ctx) {
const d = Math.hypot(dot.x - ctx.width / 2, dot.y - ctx.height / 2)
state.scale += (Math.sin(d * 0.03 - ctx.time * 3) * 0.5 + 0.5) * 0.6
},
}
dots.setOptions({ motion: { mutators: [noiseField(), ripple] } })
Tree-shaking: import only the mutators you use from dots-pattern/motion. With
sideEffects: false and no eager registry, unused mutators are dropped from your bundle.
Options
| Option | Default | Notes |
|---|---|---|
spacing |
24 |
px between dot centres |
columns |
null |
fit exactly N columns across the width (overrides spacing) |
maxRadius |
4 |
max drawn radius (px) |
minRadius |
0 |
min visible radius (px); 0 lets dots vanish |
baseScale |
0 |
baseline size factor before animators add to it |
packing |
'square' |
'square' or 'hex' |
padding |
0 |
number or { top, right, bottom, left } |
color |
'#4a2b47' |
CSS string, or (ctx) => string for per-dot colours |
background |
null |
transparent when null |
opacity |
1 |
global alpha |
motion |
'noise' |
'noise' / 'static', or { mutators: [...] } |
autoplay |
true |
start animating on creation |
respectReducedMotion |
true |
render one static frame if the user prefers reduced motion |
pauseWhenHidden |
true |
pause the RAF loop while offscreen / tab hidden |
fpsCap |
null |
throttle the frame rate |
dprCap |
2 |
upper bound on devicePixelRatio |
renderer |
'canvas' |
'canvas' or a custom Renderer |
pointerTarget |
'container' |
where the pointer mutator listens: 'container' / 'window' / 'canvas' / an HTMLElement |
Pointer input & overlaying content
The pointer mutator reacts to the cursor, but the DOM listeners live on the controller.
By default (pointerTarget: 'container') they bind to the mount element the canvas
fills, not the canvas itself — so you can stack hero text, links and a CTA on top with
their normal pointer events intact and the dots still track the cursor across the whole
area. No pointer-events: none overlay hack required. Use 'window' to keep tracking the
cursor beyond the container, 'canvas' for the legacy "only over bare canvas" behaviour,
or pass any HTMLElement to listen on. When mounting onto an existing <canvas>,
'container' resolves to that canvas's parent element.
Built-in mutators
noiseField({ speed, morph, waveSize, octaves, persistence, lacunarity, sharpness, intensity, seed })— fractal noise field.waveSize= largest wave/hole size in px;octaves+persistencelayer smaller waves so hole sizes vary;speed= horizontal drift in px/sec (motion is horizontal only);morph= how fast the shapes evolve as they drift;sharpness(0–1) = how crisp the blob/hole edges are.sineField({ speed, wavelength, angle, intensity })pointer({ mode: 'grow' | 'shrink' | 'repel', radius, strength, ease, fadeIn, fadeOut })— near the cursor the dot's size is blended toward a target (grow → full, shrink → gone), weighted by proximity, so the cursor takes precedence over the noise locally and eases back to it at the edge ofradius.strength(0–1) is the max blend weight at the centre.repeldisplaces dots instead. Envelopes (fade/gradient) are respected. The whole effect eases in overfadeInseconds (default0.12) when a pointer arrives and fades out overfadeOutseconds (default0.6) when the cursor leaves or a touch lifts — so on touch it doesn't snap off. Works with mouse and touch alike (Pointer Events); a stationary tap activates it too.gradient({ axis: 'x' | 'y' | 'radial', from, to, start, end, ease })— scale dots by position; the default (axis: 'y') makes them bigger toward the bottom.fade({ direction: 'top' | 'bottom' | 'left' | 'right' | 'radial', start, end, ease })
WebGL renderer (high density)
The default Canvas 2D renderer is great up to a few thousand dots. For very dense grids, opt into the WebGL renderer — it draws each dot as a GPU point-sprite (antialiased circle) and handles far higher counts at 60fps. It's a separate entry point, so it only ships if you import it:
import { createDotsPattern } from 'dots-pattern'
import { WebGLRenderer } from 'dots-pattern/webgl'
createDotsPattern(el, {
spacing: 8, // dense
renderer: new WebGLRenderer(),
})
Same options, same mutators, same output — only the drawing backend changes. WebGL
context loss is handled automatically. If a WebGL context can't be created, construction
throws, so you can try/catch and fall back to the default Canvas 2D renderer.
React
import { DotsPattern } from 'dots-pattern/react'
<DotsPattern options={{ spacing: 24, color: '#4a2b47' }} className="hero-bg" />
Vue
<script setup lang="ts">
import { DotsPattern } from 'dots-pattern/vue'
</script>
<template>
<DotsPattern :options="{ spacing: 24, color: '#4a2b47' }" />
</template>
There's also a useDotsPattern(elRef, optionsRef) composable for mounting onto your own
element.
Development
npm install
npm run dev # demo playground at http://localhost:5173
npm test # unit tests (grid + options)
npm run build # library build via tsup → dist/
License
MIT
Dependencies
Development dependencies
| ID | Version |
|---|---|
| @types/react | ^19.0.0 |
| jsdom | ^29.1.1 |
| react | ^19.0.0 |
| release-it | ^20.2.1 |
| tsup | ^8.3.5 |
| typescript | ^5.7.2 |
| vite | ^6.0.5 |
| vitest | ^2.1.8 |
| vue | ^3.5.13 |
Peer dependencies
| ID | Version |
|---|---|
| react | >=17 |
| vue | >=3 |