@pouriahlabs/wisp-ui
v0.3.3
Published
Ambient motion that knows where your content is — the runtime behind the Wisp component registry.
Maintainers
Readme
@pouriahlabs/wisp-ui
The runtime behind the Wisp component registry.
You usually don't install this directly — the components pull it in:
npx shadcn@latest add https://wisp.pouriah.com/r/packet-field.jsonThat copies the component's source into your repo and adds @pouriahlabs/wisp-ui as a
dependency. The component is yours to edit; this package is the small versioned
part it imports.
npm i @pouriahlabs/wisp-uiWhat's in it
| Hook | Does |
| --- | --- |
| useCanvasScene | The loop: DPR clamping, resize, in-view and hidden-tab pausing, reduced-motion still frame, teardown |
| useObstacles | Measures the content boxes a scene must not draw over |
| useTokenColor | Reads design tokens as HSL, and re-reads them when the theme changes |
| useScriptedSequence | The same lifecycle contract for DOM-driven demo surfaces |
| useFlip | First / Last / Invert / Play, for layout that moves |
| useReducedMotion | One answer to "should this page hold still?" |
A minimal scene
"use client";
import * as React from "react";
import { hsl, useCanvasScene, useObstacles, useTokenColor } from "@pouriahlabs/wisp-ui";
export function Motes() {
const ref = React.useRef<HTMLCanvasElement>(null);
const boxes = useObstacles(ref);
const wisp = useTokenColor("--wisp", [162, 72, 52]);
const motes = React.useRef<{ x: number; y: number; vy: number }[]>([]);
useCanvasScene(ref, {
setup: ({ width, height }) => {
motes.current = Array.from({ length: 40 }, () => ({
x: Math.random() * width,
y: Math.random() * height,
vy: 12 + Math.random() * 30,
}));
},
draw: ({ ctx, width, height, dt }) => {
ctx.clearRect(0, 0, width, height);
for (const m of motes.current) {
m.y -= m.vy * dt;
if (m.y < 0) m.y = height;
// The point of the library: skip anything behind real content.
if (boxes.current.some((b) => m.x > b.l && m.x < b.r && m.y > b.t && m.y < b.b)) continue;
ctx.fillStyle = hsl(wisp, wisp[2] + 20, 0.7);
ctx.fillRect(m.x, m.y, 2, 2);
}
},
});
return <canvas ref={ref} aria-hidden className="pointer-events-none absolute inset-0 h-full w-full" />;
}Then tag the content it should avoid:
<h1 data-wisp-avoid>…</h1>
<p data-wisp-avoid>…</p>Tag elements individually rather than one wrapper — the bands between them stay open, and motion threading through those gaps is most of what makes the effect read as deliberate.
Tokens
Wisp reads bare HSL channels, the shadcn/ui convention:
:root {
--wisp: 162 72% 52%;
--lantern: 42 88% 62%;
--wisp-gain: 1; /* canvas alpha multiplier; lower it in light themes */
}The contract
Every component in the registry certifies against this, and the hooks are how it's satisfied by construction rather than by discipline:
- Reduced motion paints one composed still frame — never a blank box
- Pauses off-screen and in hidden tabs
- Re-reads tokens on theme change
- SSR-safe, no hydration mismatch, no layout shift
- Decorative layers are
aria-hiddenandpointer-events-none - DPR clamped
- Zero runtime dependencies beyond React
MIT © PouriahLabs
