Animated topographic contour backgrounds. One React component, zero dependencies, drawn on the GPU.
Interactive playground · Try it on CodeSandbox
npm i topolines
import { Topolines } from "topolines/react";
export default function Hero() {
return <Topolines seed="topolines" color="#F2EFE6" style={{ position: "fixed", inset: 0 }} />;
}import { TopoField } from "topolines";
const field = new TopoField(document.getElementById("host")!, {
seed: "topolines",
color: "#F2EFE6",
});
// later
field.pause();
field.destroy();Classic script tag (exposes a topolines global):
<script src="https://cdn.jsdelivr.net/npm/topolines"></script>
<script>
const field = new topolines.TopoField(document.getElementById("topolines-bg"), {
seed: "hero",
});
</script>Or as an ES module, no npm and no bundler needed:
<div id="topolines-bg" style="position:fixed;inset:0"></div>
<script type="module">
import { TopoField } from "https://esm.sh/topolines";
const field = new TopoField(document.getElementById("topolines-bg"), {
seed: "hero",
});
</script>| Prop | Type | Default | Description |
|---|---|---|---|
seed |
string |
"topo" |
Seeds the noise offset. Same seed renders the same field. |
speed |
number |
0.012 |
Animation speed of the field over time. |
scale |
number |
1.15 |
Zoom level of the contour field. |
levels |
number |
11 |
Number of contour bands. |
lineWidth |
number |
1.2 |
Line thickness of each contour. |
opacity |
number |
0.16 |
Overall opacity of the drawn lines. |
color |
string |
"#C3D82C" |
Line color. Accepts any CSS color. |
drift |
[number, number] |
[0.004, 0.002] |
Constant x/y drift applied to the field per frame. |
warp |
number |
0.18 |
Amount of domain warp applied to the noise. |
scrollPan |
[number, number] |
[0, 0] |
Extra pan offset, in the same space as drift, useful for pinned sections. |
scrollEase |
number |
0.18 |
How hard the pan follows the scroll, as a fraction closed per frame at 60Hz (rescaled to the real frame rate). 1 locks it to the scrollbar, so a wheel notch moves the map ~100px in one frame; lower glides between the notches. Ignored when scrollPan is [0, 0]. |
colorStops |
{ at: number; color: string; opacity: number }[] | undefined |
undefined |
Color keyframes interpolated by getProgress(). Overrides color/opacity. Must be sorted by at ascending. |
getPanScroll |
() => number | undefined |
window.scrollY |
Overrides the scroll value scrollPan reads, for example to freeze pan across a pinned section. |
getProgress |
() => number | undefined |
document scroll progress | Drives colorStops. Defaults to the page scroll fraction. |
maxDpr |
number |
1.5 |
Caps device pixel ratio used for rendering. |
interactive |
boolean |
false |
Enables the cursor bump: contour rings bloom around the pointer. |
mouseStrength |
number |
0.35 |
Strength of the cursor bump when interactive is on. |
mouseRadius |
number |
0.35 |
Radius of the cursor bump when interactive is on. |
fallback |
ReactNode |
null |
Rendered instead of the canvas when WebGL is unsupported (React only). |
className |
string | undefined |
undefined |
Class applied to the host element (React only). |
style |
CSSProperties | undefined |
undefined |
Style applied to the host element (React only). |
- SSR-safe. No
window/documentaccess at module load, safe to import in a Next.js server component tree. - Pauses automatically when the canvas is offscreen or the tab is hidden, and resumes when visible again.
- Respects
prefers-reduced-motion: renders one static frame instead of animating. - Requires WebGL1 with the
OES_standard_derivativesextension. Check ahead of time withisSupported(), or pass afallbackto the React component for automatic handling. - If the environment lacks support, the component mounts nothing and reports
isSupported() === false/ aTopoFieldinstance with.ok === false. - Recovers from WebGL context loss. Browsers cap how many contexts a page may keep and evict the oldest, which matters on a page with several fields. The field rebuilds its program and resumes on its own;
TopoField.contextLostreports the state in the meantime. - Pointer listeners are attached only while
interactiveis on, and the element rect is cached rather than measured per frame or per pointer event: it is re-read when the element resizes, the page scrolls, the pixel density changes, or half a second has passed. The frame loop itself does no layout reads at all. - Every follow in the engine - the cursor bump, its position, the scroll pan - is rescaled to the real frame delta, so the motion is identical at 60, 120 and 144Hz instead of settling more than twice as fast on a high-refresh display.
Docs and interactive playground: https://topolines.idlee.xyz Source: https://github.com/idleCyrex/topolines
MIT. Includes a simplex noise implementation from webgl-noise, by Ashima Arts and Stefan Gustavson (MIT). See LICENSE.