hexagons
Animated honeycomb backgrounds built from regular hexagons. Line art on canvas —
the colour gradient runs along the edges — plus a filled low-poly mode when you
want the opposite. Zero dependencies, ~6.5 KB gzipped.
Three modes: hexagons drifting toward the viewer in depth, the living honeycomb
lattice, or a faceted crystal fill that assembles itself and freezes. And two
knobs that span the whole variant space: seed spins the geometry,
brand spins the colours.

Live demo →
Install
npm install hexagons-lite
Or straight from a CDN, no build step:
<script src="https://cdn.jsdelivr.net/npm/hexagons-lite@0.1/hexagons.min.js"></script>
Then:
<div class="bg"></div>
<script>
Hexagons.init('.bg', { mode: 'hive' });
</script>
The script defines a single global, Hexagons. There is no module build — it is a
browser script, and main points at the unminified source for bundlers that inline it.
The container needs a size of its own — the canvas fills it. A typical hero:
.hero { position: relative; min-height: 100vh; }
.bg { position: absolute; inset: 0; z-index: 0; }
.hero > .content { position: relative; z-index: 1; }
The live demo is index.html in this repo — a playground with every option wired to
a control, in both themes.
Auto-palette: one colour in, the whole scene out
You do not pick five colours. You pass your brand's one:
Hexagons.init('.bg', { brand: '#7c5cff' });
The library derives the background, halo, gradient stops, accent, and sweep colour
from that single hex — in CIE LCh, so hue stays your hue; contrast floors keep the
lines visible; out-of-gamut colours lose chroma, never shift hue. Multi-colour
brands pass an array — the first entry drives the field, the second becomes the
accent, the third tints the light sweep:
Hexagons.init('.bg', { brand: ['#00abf3', '#d6af3c', '#a91455'] });
Two themes from the same brand: theme: 'dark' (default) or 'light'. Explicit
colour options always win over derivation, and stay pinned across set({ brand });
pass 'auto' to hand one back. The derivation itself is public and pure:
Hexagons.palette('#7c5cff')
Hexagons.palette('#7c5cff', { theme: 'light' })
Grey-on-purpose brands stay grey: an achromatic seed derives a graphite palette —
no hue is ever invented.
Modes
field — depth
Regular hexagons scattered through depth, drifting toward the camera, overlapping at
different scales. Each one faces the viewer, so it stays exactly regular at any
distance. Some carry a concentric inner ring, a few use the accent colour, and the
field drifts with the pointer.
Hexagons.init('.bg', {
mode: 'field',
count: 110,
speed: 1,
size: 90
});
hive — the honeycomb
The 6.6.6 tiling — the real thing, not an approximation. A soft band of light sweeps
across the walls.
Hexagons.init('.bg', {
mode: 'hive',
bond: 0.15,
orientation: 'flat'
});
bond is the probability of dropping a shared wall, which merges two cells into one
outline. Useful range is roughly 0.10–0.20; past ~0.4 the comb falls apart. The
selection is deterministic, so bonds never flicker.
inset shrinks every cell by a few pixels, so neighbouring outlines become parallel
double walls — the read of a real comb. In v0.1 bond and inset are mutually
exclusive (bond > 0 wins): fused cells form arbitrary polyhex shapes, and
offsetting their union outline costs more bytes than it is worth. Yet.
fill — the crumpled crystal
Not line art: a filled faceted honeycomb. Every cell fans into six flat-shaded
facets over a diagonal brand gradient, assembles itself once with a staggered
entrance, then stays a completely static painting — zero CPU after the reveal.
Hexagons.init('.bg', {
mode: 'fill',
facets: 'crystal',
chaos: 0.5,
depth: 0.4,
animation: {
effect: 'scale',
direction: 'top',
duration: 1500,
stagger: 0.6
}
});
Three looks from two knobs: facets: 'crystal' is the gemstone low-poly;
facets: 'cells' keeps every hexagon whole — an organic living comb; and
facets: 'cells', chaos: 0 is the strict comb — a quantised brand gradient of
perfect hexagons.
animateIn() / animateOut() replay the entrance or dissolve the surface — flip
themes the classy way: animateOut(), set({ theme: 'light' }), animateIn().
The mesh is deterministic (no flicker, resize only recentres it), and a seed
makes the entrance reproducible frame-by-frame through step(dt).
Static pattern — no canvas at all
The honeycomb is periodic on a size × √3·size rectangle, so pattern() returns a
seamless SVG tile as a ready-to-use background-image:
document.querySelector('pre').style.backgroundImage =
Hexagons.pattern({ size: 18, opacity: 0.09 });
Nothing animates and nothing runs afterwards — it is one tile handed to CSS.
size | 18 | Width across flats in px; the repeat tile is size × √3·size |
brand | spintax triad | Auto-palette seed; color defaults to the derived middle stop |
theme | 'light' | Derivation profile (patterns usually sit on light surfaces) |
color | derived | Stroke colour; explicit value overrides derivation |
opacity | 0.12 | Stroke opacity — the contrast knob |
weight | 1 | Stroke width |
orientation | 'pointy' | 'pointy' or 'flat' |
inset | 0 | Cell shrink in px (double-wall look) |
background | null | Optional solid fill behind the lines |
raw | false | Return bare <svg> markup instead of url(...) |
Six corners at 120° survive small pitches well — hexagons stay legible down to
roughly 14 px, where an octagon needs 24.
Offline rendering
The animation can be driven on your own clock, reproducibly:
var hx = Hexagons.init('.bg', {
seed: 42,
background: null,
parallax: false,
autoplay: false
});
hx.stop();
for (var i = 0; i < 600; i++) {
hx.step(1 / 60);
}
With background: null the vignette switches itself off (it would bake into the
alpha channel otherwise) — pin vignette explicitly if you really want it. Frame
identity is per environment: a fixed browser build, viewport, and DPR.
step(dt), not render(absoluteTime) — field motion is integrated and respawns
re-randomise, so frames must be produced in order from the start.
Options
mode | 'field' | — | 'field', 'hive', or 'fill' |
facets | 'crystal' | fill | 'crystal' (faceted) or 'cells' (whole hexagons) |
chaos | 0.5 | fill | Vertex jitter, 0–1 |
depth | 0.4 | fill | Facet lighting strength |
animation | scale/top | fill | Entrance animation object; null = instant |
brand | spintax triad | both | Auto-palette seed — hex or array of hexes |
theme | 'dark' | both | 'dark' or 'light' derivation profile |
colors | derived | both | Gradient stops along the edges |
accent | derived | field | Colour of the occasional highlighted cell |
hot | derived | hive | Colour of the sweeping light band |
background | derived | both | Base fill. null = transparent canvas |
halo | derived | both | Soft central glow. null disables |
size | 90 | both | Width across flats in px; scale factor for the field |
count | 110 | field | Number of hexagons |
seed | none | field | Integer; reproducible scatter. Settable live |
speed | 1 | both | Animation rate; 0 freezes |
weight | 1 | both | Line thickness multiplier |
glow | true | both | Soft halo around bright edges |
sweep | 1 | hive | Light-band speed; 0 disables |
bond | 0 | hive | Probability of fusing neighbours |
orientation | 'pointy' | hive | 'pointy' or 'flat' |
inset | 0 | hive | Double-wall gap in px |
nesting | true | field | Concentric inner rings on some cells |
parallax | true | field | Pointer-driven drift |
vignette | 0.45 | both | Edge darkening; auto-0 on a transparent canvas |
maxDpr | 2 | both | Device-pixel-ratio ceiling (init-only) |
autoplay | true | both | Start immediately (init-only) |
API
init() returns null if the selector matches nothing, otherwise:
var hx = Hexagons.init('.bg');
hx.set({ mode: 'hive', bond: 0.2 });
hx.set({ brand: '#e0356b' });
hx.set({ accent: 'auto' });
hx.get();
hx.stop();
hx.start();
hx.step(1 / 60);
hx.resize();
hx.destroy();
hx.canvas;
Performance
The things that actually cost frames in a full-screen canvas background:
- It sleeps when off screen. An
IntersectionObserver pauses the loop when the
container scrolls out of view, and page-visibility pauses it on a hidden tab.
- Gradients are built once, not per frame.
- Glow avoids
shadowBlur — a wide dim stroke under a thin bright one gives the
same look for roughly a third of the cost.
- Device pixel ratio is capped at 2.
About the geometry
The regular hexagon is one of exactly three regular tilings of the plane — and the
best one. The honeycomb conjecture (proved by Thomas Hales, 1999) says the
hexagonal grid is the least-perimeter way to divide the plane into equal areas:
~7% less edge than squares, ~18% less than triangles, less than any exotic
curved-wall partition.
This library draws only perimeters — nothing is filled, every photon comes from
an edge. So the hexagon is literally the cheapest tiling in ink: the mathematically
optimal shape for line art. Its sibling library, octagons,
was built around the opposite fact — a regular octagon cannot tile at all.
There is a reason nature keeps arriving here: planar cells average exactly six
sides (a consequence of Euler's formula), which is why bee combs, basalt columns,
and dragonfly wings all relax into hexagons.
Browser support
Any browser with <canvas>. ResizeObserver and IntersectionObserver are used
when available and degrade gracefully without them.
Credits
Built by 301ST for spintax.net — a free,
open-source template engine that expands one template into thousands of unique,
deterministic text variants. This library applies the same idea to geometry: one
seed is a whole hive, one brand is a whole palette.
License
MIT © 301ST