MindSprout Studios · labOctober 2026field guide · vol. 1

Uncommon web

79 web libraries and components with a strong point of view: dithered shaders, text laid out without the DOM, Windows 98 as a stylesheet, ink that bleeds, synths you can play. Every card below is running the real library, live.

live demos
80
families
9
libraries
79
CDN requests
0
01 ▦

Dither, pixel & ASCII

9 demos

Ordered dithering, error diffusion, halftones, CRT phosphor and live ASCII. The look of 1-bit screens and newsprint, rebuilt in shaders and canvas.

live demo switch Idle / Run / Overdrive · scrub the bars · hover a fault square ↗ full window
reactshadcnchartssvgcanvasretro

dithercn

Press 04: live 1-bit ops dashboard

dithercn's chart kit: bars, gauges, heatmaps and sparklines whose fills are threshold-matrix dither cells instead of flat colour.

The dithercn charts encode value as dither density: a heatmap square steps from four dots to a checkerboard to a hollow frame to solid, a gauge fills block by block, and bar fills thin out through Bayer levels under a solid cap. This board simulates a web-offset press on its night run and pushes a new minute into every chart on each tick; the feed-rate buttons change the tick rate, the speed target and how often faults land on the heatmap.

npx shadcn@latest add https://dithercn.dev/r/bar-chart.json https://dithercn.dev/r/gauge.json https://dithercn.dev/r/heatmap.json https://dithercn.dev/r/sparkline.json https://dithercn.dev/r/uptime-bar.json https://dithercn.dev/r/dither-number.json
the code that drives it
<Gauge value={s.speed} min={0} max={14} variant="dial" size={190}
  label={hot ? "over rated speed" : "rated 12.0"} color={hot ? "red" : "ink"}
  trackColor="grey" fill="gradient" cell={2} />

<BarChart data={s.bars} x="t" layout="stacked" highlight="last" height={176}
  series={[{ label: "Good copies", value: "copies", color: "ink" },
           { label: "Spoils", value: "spoils", color: "red" }]}
  fill="gradient" legend={false} animate={false} cell={2} />

<Heatmap mode="matrix" data={s.heat} rows={UNITS} columns={columns}
  steps={4} max={4} unit={{ one: "fault", other: "faults" }} color="red" />
site ↗source ↗MIT · vregistry snapshot 2026-10-22 (no npm version; fetched from dithercn.dev/r)
live demo move over the field to steer · hold to zoom · change shape, matrix and ink ↗ full window
reactwebglshadergenerativeretro

Paper Shaders

Proof Nº 07: riso dithering screen

Zero-dependency WebGL shaders from Paper, the design tool, with React wrappers whose props are uniforms.

Paper's Dithering shader generates its own source shape (sphere, simplex, warp, dots, wave, ripple, swirl) and thresholds it in a single pass with 2x2, 4x4 or 8x8 Bayer matrices or random noise, in exactly two colours. Every prop is a live uniform, so this proof sheet eases the pointer into offsetX/offsetY and rotation, and holding the pointer down raises speed and scale; the ticket swaps shape, matrix, dot size and riso ink/stock pairs.

npm i @paper-design/shaders-react
the code that drives it
<Dithering
  colorBack={ink.paper}
  colorFront={ink.ink}
  shape={shape}            // sphere | simplex | warp | dots | wave | ripple | swirl
  type={type}              // 2x2 | 4x4 | 8x8 | random
  size={size}
  speed={playing ? 0.6 + steer.press * 1.4 : 0}
  scale={(objectShape ? 0.72 : 1.1) * (1 + steer.press * 0.9)}
  offsetX={steer.x}
  offsetY={steer.y}
  rotation={steer.x * 25}
/>
site ↗source ↗npm ↗Apache-2.0 · v0.0.82
live demo drag to spin the can · move to move the lamp · freeze & select the text ↗ full window
vanilla3dasciitextwebgl

three.js AsciiEffect

Can Control: a 3D spray can typed in ASCII

The ASCII effect from three.js's examples/jsm add-ons: renders a scene, samples the frame and writes it out as an HTML table of characters.

AsciiEffect replaces the renderer's canvas with a <table> of text: each frame it draws the WebGL output into a small 2D canvas, maps every sample's luminance to a character ramp and rewrites the cell's innerHTML, so the output is real, selectable, copyable text. The demo lathes a spray can from 19 profile points with a procedurally painted label, lights it with a lamp that follows the pointer, and rebuilds the effect to switch charsets or flip between paper (shadows take ink) and screen (inverted, highlights glow).

npm i three
the code that drives it
function buildEffect() {
  const screen = state.mode === "screen";
  // Paper: white sky prints as blank, shadows take the most ink.
  // Screen: black sky is blank, highlights take the densest glyphs.
  renderer.setClearColor(screen ? 0x000000 : 0xffffff, 1);
  if (effect) effect.domElement.remove();
  effect = new AsciiEffect(renderer, CHARSETS[state.charset], { invert: screen, resolution: RES });
  host.appendChild(effect.domElement);
  resize();
}

stage.addEventListener("pointermove", (e) => {
  const r = stage.getBoundingClientRect();
  const nx = (e.clientX - r.left) / r.width * 2 - 1, ny = (e.clientY - r.top) / r.height * 2 - 1;
  key.position.set(nx * 7, -ny * 5, 6);   // the lamp follows the pointer
});
live demo drag the red seams · tap the sky to move the sun · switch dot size and ink ↗ full window
vanillacanvasretrogenerative

canvas-dither

Mesa, dusk, printed three ways

Niels Leenheer's tiny 1-bit dithering toolkit for canvas ImageData

canvas-dither turns any ImageData into 1-bit with threshold, Bayer 4×4, Floyd–Steinberg or Atkinson, in about 150 lines meant for receipt printers. Here a procedural desert is dithered by all three real kernels every frame and composited behind two draggable seams, so you can see Atkinson's lost error blow out the sky while Floyd–Steinberg keeps the gradient and Bayer lays down its cross-hatch.

npm i canvas-dither
the code that drives it
import CanvasDither from 'canvas-dither';

const base = sctx.getImageData(0, 0, w, h);
const copy = () => new ImageData(new Uint8ClampedArray(base.data), w, h);
const fs = CanvasDither.floydsteinberg(copy());
const at = CanvasDither.atkinson(copy());
const by = CanvasDither.bayer(copy(), 128);
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
  const i = (y * w + x) * 4;
  const v = (x < a ? fs : x < b ? at : by).data[i];
  const c = v > 127 ? paper : ink;            // recolour the 1-bit result
  o[i] = c[0]; o[i + 1] = c[1]; o[i + 2] = c[2]; o[i + 3] = 255;
}
source ↗npm ↗MIT · v1.0.1
live demo move to swing the flashlight · switch machine and dither kernel ↗ full window
vanillacanvasretrogenerative

image-q

Corner store at 2:47 a.m., four machines

Complete TypeScript image quantisation: palette builders, colour distances and 11 dithering kernels

image-q separates the three decisions most tools hide: how to measure colour distance (CIEDE2000, BT.709, pngquant…), how to build a palette (NeuQuant, Wu, RgbQuant) and how to map pixels (nearest, Riemersma Hilbert-curve, Floyd–Steinberg, Atkinson, Stucki…). This demo skips palette building and feeds it fixed hardware palettes, Game Boy DMG, CGA mode 4, PICO-8 and a two-ink riso with overprint, re-quantising a hand-placed pixel-art storefront, drawn on the target pixel grid so only the light ramps get dithered, at a low per-machine width while a flashlight follows the pointer.

npm i image-q
the code that drives it
import { applyPaletteSync, utils } from 'image-q';

const palette = new utils.Palette();
for (const hex of PRESETS[preset].colors) {
  const [r, g, b] = hexToRgb(hex);
  palette.add(utils.Point.createByRGBA(r, g, b, 255));
}
const pc = utils.PointContainer.fromHTMLCanvasElement(src);
const q = applyPaletteSync(pc, palette, {
  colorDistanceFormula: 'euclidean-bt709-noalpha',
  imageQuantization: quant, // 'atkinson' | 'riemersma' | 'stucki' ...
});
octx.putImageData(new ImageData(new Uint8ClampedArray(q.toUint8Array()), w, h), 0, 0);
live demo drag to spin the stack · ASCII / PIXEL / RAW · cell size, glyph ramp, invert ↗ full window
3dwebglshaderasciiretro

postprocessing (pmndrs)

The speaker stack, set in type

pmndrs' effect-merging post-processing library for three.js

postprocessing merges many effects into a single fullscreen shader pass. Its ASCIIEffect looks each cell's luminance up in an ASCIITexture, a glyph atlas rasterised from any font at runtime, so here the atlas is cut from self-hosted Space Mono and you can swap character ramps live. A pumping three.js speaker stack runs through ASCII, PixelationEffect or raw, screen-blended onto ultramarine.

npm i postprocessing three
the code that drives it
import { EffectComposer, RenderPass, EffectPass, ASCIIEffect, ASCIITexture, PixelationEffect } from 'postprocessing';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// ASCIITexture builds its font as `${fontSize}px ${font}`: a weight prefix gives a bold atlas
const makeTexture = chars => new ASCIITexture({
  characters: chars, font: '"Space Mono"', fontSize: '700 62', size: 1024, cellCount: 16,
});
const INKS = { cream: 0xfffbe8, acid: 0xe8ff3a, scene: null };
const ascii = new ASCIIEffect({ asciiTexture: makeTexture(' .:-=+*#%@'), cellSize: 12, color: INKS.cream });
const asciiPass = new EffectPass(camera, ascii);
const pixelPass = new EffectPass(camera, new PixelationEffect(11));
composer.addPass(asciiPass); composer.addPass(pixelPass);
// swap glyph ramps and ink live
ascii.asciiTexture = makeTexture(' /\\|XWM');
ascii.color = INKS.acid;
live demo drag the picture to roll the vertical hold · change channel · ride the sliders ↗ full window
webglcanvasretroshadermotion

pixi-filters

Channel 3 after midnight

The official PixiJS filter collection: 40-odd WebGL/WebGPU filters for display objects

pixi-filters ships CRT, old-film and RGB-split as stackable filters on any PixiJS display object, each with live uniforms (curvature, scanline width, noise, scratch density, per-channel offsets). The SMPTE test card and the VHS tape scene are drawn live with PixiJS Graphics into a RenderTexture shown through a TilingSprite, so dragging rolls the vertical hold like a real set.

npm i pixi.js pixi-filters
the code that drives it
import { CRTFilter, OldFilmFilter, RGBSplitFilter } from 'pixi-filters';

const crt = new CRTFilter({ curvature: 2.5, lineWidth: 1.6, noise: 0.18, vignetting: 0.32 });
const film = new OldFilmFilter({ sepia: 0.12, scratch: 0.45, scratchDensity: 0.35 });
const split = new RGBSplitFilter({ red: { x: -3, y: 0 }, green: { x: 0, y: 1 }, blue: { x: 3, y: 0 } });
screen.filters = [split, film, crt];

app.ticker.add(t => {
  app.renderer.render({ container: world, target: rt });
  screen.tilePosition.y = roll;      // vertical hold
  crt.time += t.deltaMS / 1000 * 2.4; crt.seed = Math.random();
  film.seed = Math.random();
});
02 ◎

Shaders & WebGL worlds

9 demos

Fragment shaders packaged so you can use them without writing GLSL: fluid ink, mesh gradients, globes, SDF sculptures and transitions.

live demo drag the globe · pick a city ↗ full window
webgl3dshadervanillacss

COBE

The Dubplate Atlas

A ~5KB WebGL globe that draws the earth as a Fibonacci dot lattice in a single fragment shader.

COBE draws no geometry: it ray-marches a sphere in a fragment shader and samples a tiny embedded land mask on a spherical Fibonacci lattice, so the whole globe is one draw call. v2 adds instanced arcs and exposes each marker as a CSS anchor (--cobe-<id>) plus a visibility variable. The airport-style tags here are plain DOM positioned with CSS anchor positioning, and they hide as cities rotate behind the planet. Drag to spin, or pick a city in the editorial index to fly there.

npm i cobe
the code that drives it
const globe = createGlobe(canvas, {
  devicePixelRatio: dpr, width: size, height: size,
  phi, theta, dark: 1, diffuse: 1.15, mapSamples: 22000, mapBrightness: 5.2,
  baseColor: [0.2, 0.19, 0.17], glowColor: [0.86, 0.82, 0.74],
  markers, arcs, arcColor: MUSTARD, arcWidth: 0.9, arcHeight: 0.32,
});
// each marker id becomes a CSS anchor: --cobe-<id>
tag.style.positionAnchor = `--cobe-${c.id}`;
tag.style.opacity = `var(--cobe-visible-${c.id}, 0)`;
function frame() {
  if (target) { phi += (target[0] - phi) * 0.075; theta += (target[1] - theta) * 0.075; }
  globe.update({ phi, theta });
  raf = requestAnimationFrame(frame);
}
live demo move or drag across the poster · switch posters and brush ↗ full window
webglshadercanvasvanillacursor

OGL

Wet Paste

A minimal, dependency-free WebGL library that stays close to the raw API, with handy extras like Flowmap and GPGPU.

OGL's Flowmap extra ping-pongs a half-float render target. The pointer stamps velocity into it and the stamp decays every frame, giving a cheap 'memory' of motion that any shader can sample. Here that velocity field displaces a wheatpasted wall drawn procedurally in Canvas 2D (concrete grain, torn stock, halftone, Syne type). Each ink channel is pushed a slightly different distance, and fresh paths darken like wet glue. Three posters and three brush sizes (the Flowmap falloff uniform) are switchable.

npm i ogl
the code that drives it
const flowmap = new Flowmap(gl, { falloff: 0.32, dissipation: 0.965, size: 256 });
// fragment: smear the paper by the stored velocity
vec3 flow = texture2D(tFlow, vUv).rgb;
vec2 push = flow.xy * .2 * uCover;
float r = texture2D(tWall, uv - push * 1.06).r;
float g = texture2D(tWall, uv - push).g;
float b = texture2D(tWall, uv - push * .95).b;
// per frame
flowmap.mouse.copy(mouse);
flowmap.velocity.lerp(velocity, velocity.len() ? 0.5 : 0.1);
flowmap.update();
renderer.render({ scene: mesh });
site ↗source ↗npm ↗Unlicense · v1.0.11
live demo move to steer · tabs or keys 1–3 switch channels ↗ full window
webgl3dshadergradientvanilla

Vanta.js

Sky Channel

Drop-in animated 3D/shader backgrounds for any element, built on a pinned three.js.

Each Vanta effect is a self-contained three.js scene or full-screen shader, bound to a DOM element and steered by the pointer. The effects pin three r134, so this demo installs that exact version and passes THREE in explicitly instead of relying on a global. A broadcast-style switcher destroys and rebuilds the effect per channel (Waves, Fog, Cells), each with its own palette and copy. Pausing cancels Vanta's own rAF handle.

npm i vanta three@0.134.0
the code that drives it
import * as THREE from 'three';
import WAVES from 'vanta/dist/vanta.waves.min';
const fx = (m) => (typeof m === 'function' ? m : m.default);
function tune(name) {
  const ch = CHANNELS[name];
  if (effect) effect.destroy();
  effect = ch.make({
    el, THREE, mouseControls: true, touchControls: true,
    minHeight: 200, minWidth: 200, scale: 1, ...ch.opts,
  });
}
// uw:pause
cancelAnimationFrame(effect.req);  // resume: effect.animationLoop()
site ↗source ↗npm ↗MIT · v0.5.24 (three 0.134.0)
live demo move fast to stir the gradient · pick a grade top-right ↗ full window
reactwebglshadergradient

Paper Shaders — MeshGradient

Dusk Prospectus

Zero-dependency GLSL shader components from the makers of the Paper design tool.

Paper Shaders ships each effect as a tiny React component whose props map straight onto uniforms, with built-in grain, fit/scale/offset sizing and frame-accurate speed control. Here MeshGradient carries four art-directed colour grades; pointer velocity pumps its distortion, swirl and speed while pointer position pans its offset and rotation.

npm i @paper-design/shaders-react
the code that drives it
<MeshGradient className="mesh"
        colors={grade.colors}
        distortion={Math.min(1, grade.distortion + p.d * 0.4)}
        swirl={Math.min(1, grade.swirl + p.d * 0.5)}
        grainMixer={grade.grainMixer} grainOverlay={grade.grainOverlay}
        offsetX={grade.ox - p.x * 0.35} offsetY={(grade.oy || 0) + p.y * 0.35} rotation={grade.rot + p.x * 30}
        scale={1.15 + p.d * 0.1}
        speed={paused ? 0 : 0.35 + p.d * 1.2}
        fit="cover" style={{ position: 'absolute', inset: 0 }} />
site ↗source ↗npm ↗Apache-2.0 · v0.0.82
live demo move to tilt the sea · choose a sea area ↗ full window
reactwebgl3dgradientshader

ShaderGradient

Forecast for an Imagined Coast

Moving 3D gradients on plane, sphere and water-plane meshes, built on react-three-fiber.

ShaderGradient renders a noise-displaced mesh (plane, sphere or waterPlane) lit in 3D and viewed through a spherical camera, so its gradients have folds, rim light and parallax rather than flat colour stops. Four sea areas each get their own mesh type, camera and palette; the pointer tilts the mesh and churns its strength, and pause freezes the r3f frameloop. Uses lightType '3d' so no HDR environment maps are fetched.

npm i @shadergradient/react @react-three/fiber three three-stdlib camera-controls
the code that drives it
function Projector({ paused }) {
  const setFrameloop = useThree(s => s.setFrameloop);
  useEffect(() => { setFrameloop(paused ? 'never' : 'always'); }, [paused, setFrameloop]);
  return null;
}
// …
      <ShaderGradientCanvas style={{ position: 'absolute', inset: 0 }} pixelDensity={1} fov={45} pointerEvents="none" lazyLoad={false}>
        <Projector paused={paused} />
        <ShaderGradient control="props" lightType="3d" animate={paused ? 'off' : 'on'} {...g}
          rotationY={g.rotationY + tilt.x * 24}
          rotationX={g.rotationX + tilt.y * 18}
          uStrength={g.uStrength * (1 + Math.abs(tilt.x) * 0.6)} />
      </ShaderGradientCanvas>
live demo click to cut · drag sideways to scrub · pick a transition ↗ full window
vanillawebglshadergenerativemotion

GL Transitions

Night Exteriors

An open collection of 120+ GLSL transition shaders behind one tiny spec: vec4 transition(vec2 uv).

gl-transitions is a community catalogue of transition shaders, each a single GLSL function with typed params, author and license; gl-transition is the small runtime that draws one between two textures. Here regl paints five procedural film plates into framebuffers and gl-transition cuts between them with twelve picks from the catalogue; dragging sideways scrubs the progress uniform by hand.

npm i gl-transitions gl-transition regl
the code that drives it
const regl = createREGL({ gl });
fbos.forEach((fbo, i) => paint({ scene: i, seed: seed + i, fbo }));

// regl texture → the gl-texture2d-like object gl-transition expects
const asTex = fbo => ({ shape: [W, H], bind(unit) { gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, fbo.color[0]._texture.texture); return unit; } });
const texes = fbos.map(asTex);
const getT = name => {
  if (!cache.has(name)) { gl.bindBuffer(gl.ARRAY_BUFFER, tri); cache.set(name, createTransition(gl, transitions.find(t => t.name === name), { resizeMode: 'cover' })); }
  return cache.get(name);
  getT(fx).draw(cutting || scrub !== null ? ease(prog) : 0, texes[cur], texes[next], canvas.width, canvas.height, PARAMS[fx] || {});
site ↗source ↗npm ↗MIT · vgl-transitions 1.76.1 · gl-transition 2.1.0 · regl 2.1.1
03

Typography & text

9 demos

Text measured, split, drawn, flipped and reflowed. Libraries that treat letters as material instead of content.

live demo move across the headline · Wave / Stairs / Press presets ↗ full window
vanillacsstextmotion

Splitting.js

Broadside Nº 07: every letter is a decision

Splits text into spans and exposes each piece's index as a CSS variable

Splitting wraps every word and character in a span and writes --word-index, --char-index and --char-total onto it, so motion can be choreographed in plain CSS with calc(). This poster animates registered custom properties that drive Anybody's width and weight axes, with three presets keyed to different indices (a travelling wave, a per-beat stair climb, word-by-word presses) at 92 BPM. Glyphs near the pointer condense and invert.

npm i splitting
the code that drives it
Splitting({ target: ln, by: 'chars' }); // writes --char-index / --word-index on every span

/* style.css */
@property --w { syntax: '<number>'; inherits: false; initial-value: 100; }
.head .char {
  font-variation-settings: "wdth" calc(var(--w) - var(--near) * 30), "wght" calc(var(--g) + var(--near) * 200);
}
[data-mode=wave] .head .char {
  animation-name: wave;
  animation-delay: calc(var(--char-index) * -110ms - var(--line-index) * 300ms);
}
[data-mode=press] .head .char { animation-delay: calc(var(--word-index) * var(--beat) / 2); }
@keyframes wave { 0%,100% { --w: 56; --g: 850 } 50% { --w: 148; --g: 300 } }
live demo tap the note or ↺ to rewrite · pick an ink · move to tilt the card ↗ full window
vanillasvgtextmotion

Vara

Liner note that writes itself

Draws handwritten text as animated SVG strokes from single-line JSON fonts

Vara ships its own stroke fonts: JSON files of single-line pen paths, not outlines, so text can be drawn the way a hand moves rather than revealed by a mask. This J-card liner note is written in its Shadows Into Light font, vendored locally, with Vara's per-stroke animate() hook routed through a pausable clock so the pen stops when the page asks. Replay, switch ink, or tilt the card with the pointer.

npm i vara
the code that drives it
const v = new Vara('#pen', fontUrl, blocks, { color: ink, autoAnimation: false, queued: false });
// Vara hands each stroke to animate(); we drive it from our own pausable clock
v.animate = (elem, duration, delay, final) => {
  const bi = containers.findIndex(c => c.contains(elem));
  tweens.push({ elem, start: blockStart[bi] + delay, dur: duration,
    from: parseFloat(elem.style.strokeDashoffset), to: Number(final) || 0 });
};
v.ready(() => {
  containers = blocks.map((_, i) => v.get(i).container);
  blocks.forEach((_, i) => v.draw(i));
});
live demo tap any tile to nudge it · Drop it / Breakdown · toggle Continuous ↗ full window
vanillaweb componentstextmotion

NumberFlow

The Basement live board

A web component that animates formatted numbers with rolling digits

NumberFlow formats values through Intl.NumberFormat, diffs the parts, and rolls only the digits that changed, so currency, percent and decimals all animate correctly. This live gig board uses a number-flow-group so neighbouring numbers move together, the continuous plugin so jumps visibly pass through every digit, a fixed downward trend for the countdown, and digits: {1: {max: 5}} so the seconds roll 00→59 correctly.

npm i number-flow
the code that drives it
import 'number-flow';
import 'number-flow/group';
import { continuous } from 'number-flow';

for (const f of Object.values(flows)) {
  f.el.format = f.fmt;            // e.g. { style: 'currency', currency: 'USD' }
  f.el.trend = (a, b) => Math.sign(b - a);
  if (f.digits) f.el.digits = f.digits; // seconds: { 1: { max: 5 } }
  f.el.plugins = [continuous];
  f.el.update(f.v);
}
$('mm').trend = -1; $('ss').trend = -1; // a countdown always rolls downward
live demo type a tannoy message + enter · tap a row to change its remark · switch boards top right ↗ full window
web componentstextretromotion

split-flap-elements

Night departures board

Dependency-free, accessible split-flap Web Components: <sfe-board> and <sfe-cell>.

Each <sfe-cell> owns a reel, any array of strings, so one cell can flip through whole words like BOARDING or SOLD OUT while its neighbours flip letters. A board choreographs its cells through sequence frames with settle orders such as center-out and edges-in, and it supports pause and resume. This board runs six rows of Shadow-DOM cells styled only through --sfe-* custom properties, plus a typed tannoy ribbon. Remarks are recoloured on the sfe-settle event.

npm i split-flap-elements
the code that drives it
import 'split-flap-elements';

const c = document.createElement('sfe-cell');
c.setAttribute('name', 'rem');
c.setAttribute('span', 6);
c.reel = [' ', 'ON TIME', 'BOARDING', 'DELAYED', 'LAST CALL', 'GONE'];
board.append(c);

function show(board, values, order = 'forward', stagger = 45) {
  board.stop();
  board.sequence = [{ values, settleOrder: order, stagger, hold: 600,
                      timing: { flipDuration: 38, spinDuration: 700 } }];
  board.play();
}
show(row, { t0: '2', t1: '3', d0: 'C', d1: 'A', rem: 'BOARDING' }, 'center-out');
document.addEventListener('sfe-settle', e => { /* colour remark by meaning */ });
live demo move the pointer: x = slant, y = stroke weight · hover a marquee cell for its bitmask · type into feed ↗ full window
canvasretrotextvanilla

segment-display (sixteensegment)

SX-1979 segment clock & marquee

Canvas seven- and sixteen-segment displays, where every glyph is a 16-bit mask.

The package wraps a SegmentCanvas engine: segment geometry is computed from SegmentWidth, SegmentInterval and BevelWidth, and each character is a bitmask in CharacterMasks. The demo takes the SevenSegment and SixteenSegment prototypes from the package's own custom elements and drives them directly. Three seven-segment pairs make the clock and a sixteen-segment marquee scrolls below. The pointer recomputes stroke width and slant live, and new glyphs are added by OR-ing segment bits.

npm i segment-display
the code that drives it
import 'segment-display/dist/bundle.min.js';

const SIXTEEN = Object.getPrototypeOf(donors[0].segment);
const ElementArray = SIXTEEN.ElementArray.constructor;
const seg = n => 1 << n;               // a1 a2 b c d1 d2 e f g1 g2 h i j k l m
SIXTEEN.CharacterMasks['/'] = seg(12) | seg(15);

const marquee = Object.create(SIXTEEN);
marquee.ElementArray = new ElementArray(12);
marquee.SegmentWidth = stroke;          // pointer y
marquee.CalcPoints();
ctx.setTransform(dpr, 0, -Math.tan(slant) * dpr, dpr, skewX, 0); // pointer x
marquee.ElementArray.SetText(frameText(), marquee.CharacterMasks);
marquee.Draw(canvas, marquee.ElementArray.Elements);
live demo hover a word to read its grammar scope · ← → or arrow keys to change theme ↗ full window
textvanilla

shiki

One poem, eight themes

Syntax highlighting with real TextMate grammars and VS Code themes.

Shiki tokenizes with the same TextMate grammars and theme files that VS Code uses, so colour comes from scope selectors rather than regex guesses. This demo uses the fine-grained core: one JavaScript grammar, eight less common themes as ESM imports, and the pure-JS regex engine, so nothing loads WASM or fetches at runtime. codeToTokens with includeExplanation returns every token's scope stack. The poem is rendered by hand, its palette is measured by ink coverage, and hovering any word shows the scope that coloured it.

npm i shiki
the code that drives it
import { createHighlighterCore } from 'shiki/core';
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
import javascript from '@shikijs/langs/javascript';
import houston from '@shikijs/themes/houston';
import laserwave from '@shikijs/themes/laserwave';

const hl = await createHighlighterCore({
  themes: [houston, laserwave /* … */], langs: [javascript],
  engine: createJavaScriptRegexEngine(),
});
const { tokens, fg, bg } = hl.codeToTokens(POEM, {
  lang: 'javascript', theme: 'laserwave', includeExplanation: 'scopeName',
});
for (const t of tokens.flat())
  span(t.content, t.color, t.explanation[0].scopes.map(s => s.scopeName));
live demo tap to paste the next sheet · hover a line to re-scramble · pick a scramble charset ↗ full window
textmotionasciivanilla

baffle + fitty

Fly-poster terminal

baffle obfuscates and reveals DOM text; fitty snugly sizes text to its container.

fitty measures each headline line and sets the font size that fills the measure. The demo then shares the sheet's height between lines: short lines are capped and letter-spaced out to the edge, giving a justified wheat-paste stack. baffle then scrambles every line and reveals it on a stagger from a chosen glyph set (letters, block elements, slashes or digits). Mutation observing is turned off so the scramble doesn't trigger a refit.

npm i baffle fitty
the code that drives it
import baffle from 'baffle';
import fitty from 'fitty';

fits = spans.map(s => fitty(s, { maxSize: 2000, multiLine: false, observeMutations: false }));
fits.forEach(f => f.fit({ sync: true }));
const want = spans.map(s => parseFloat(s.style.fontSize));
// share the height: short lines get what they ask for, the rest split what's left
[...want.keys()].sort((a, b) => want[a] - want[b])
  .forEach(i => { alloc[i] = Math.min(want[i], budget / left); budget -= alloc[i]; left--; });
fits = spans.map((s, i) => fitty(s, { maxSize: alloc[i], observeMutations: false }));

active = spans.map((s, n) =>
  baffle(s, { characters: '█▓▒░▚▞▙▟', speed: 55 })
    .start().reveal(700 + n * 120, 150 + n * 260));
site ↗source ↗npm ↗MIT (baffle) · MIT (fitty) · vbaffle 0.3.6 · fitty 2.4.2
04 ✎

Hand-drawn & sketchy

8 demos

Wobble, pressure, bleed and graphite. Rendering that admits a human hand was involved.

live demo switch fill styles · drag the roughness · hover bars and slices · swap the pen ↗ full window
vanillasvgcharts

rough-viz

Season log: a sketchy dashboard

D3 chart types (bar, donut, line, scatter and more) rendered through rough.js, so they look hand-sketched.

rough-viz puts D3 scales and axes under rough.js, so every bar, slice and axis is a randomised hand-drawn path with a real fill style (hachure, cross-hatch, zigzag, dots). It also ships two handwriting fonts embedded as data URIs, so nothing loads from the network. The dashboard logs one graffiti writer's season (cans per month, where July's 63 cans went, book vs. wall hours), and the controls redraw all three charts live with a new fill style, roughness or font.

npm i rough-viz
the code that drives it
new Bar({
  ...common, element: '#bars', data: { labels: months, values: cans },
  title: 'Cans emptied per month', color: HI, highlight: RED, stroke: INK, strokeWidth: 1.4, fillWeight: state.fill === 'dots' ? 1.2 : 1.6,
  axisStrokeWidth: 1, axisRoughness: 0.6, padding: 0.22, yLabel: 'cans',
  margin: { top: 48, right: 14, bottom: 46, left: 54 },
}),
new Donut({
  ...common, element: '#donut', data: july,
  title: s ? 'July: 63 cans' : 'Where July’s 63 cans went', colors: [HI, BLUE, INK, RED, GREEN, SAND], highlight: '#fffdf3', strokeWidth: 1.4, fillWeight: 1.4,
  roughness: Math.min(state.rough * 1.4, 6), legend: !s || true, legendPosition: 'right',
  margin: { top: 44, right: 132, bottom: 10, left: 6 },
}),
live demo flip the tabs · hover the data · lights off · toggle xkcdify ↗ full window
vanillasvgcharts

chart.xkcd

The 3 a.m. session, charted

xkcd-style hand-drawn charts (line, bar, pie, XY, radar) with a jitter filter and the xkcd font built in.

chart.xkcd draws D3 charts and runs them through an SVG displacement filter for the wobbly-ink look. It embeds its own xkcd handwriting font and tooltips, and has an `unxkcdify` switch that turns the filter off. The demo is a four-page studio zine using four of its chart types, with a night mode that recolours the stroke, background and data colours through the options object, and a toggle that shows the same chart with and without the effect.

npm i chart.xkcd
the code that drives it
beat: {
  type: 'Line',
  cfg: {
    title: 'How good the beat is, by hour of the session', short: 'How good the beat is',
    xLabel: 'time', yLabel: 'heat (self-reported)',
    data: {
      labels: ['9pm', '10', '11', '12am', '1', '2', '3', '4', '5', 'noon'],
      datasets: [
        { label: 'me', data: [3, 4, 5.5, 7, 8.2, 9.3, 9.8, 9.9, 9.6, 1.5] },
        { label: 'my engineer', data: [3, 3.4, 3.8, 4, 3.8, 3.4, 2.8, 2, 1.2, 2] },
      ],
    },
    options: { yTickCount: 4, legendPosition: upLeft },
  },
},
// ...
new chartXkcd[def.type](svg, cfg);
live demo drag across to scrub the pen · try one by one · bounce timing · re-ink ↗ full window
vanillasvgmotion

vivus

Elevated, 4:50 a.m.: inked by vivus

Animates SVG strokes with stroke-dashoffset so a drawing looks like it is being inked.

vivus measures every path and animates its dash offset. In its 'scenario' mode, each path's data-start and data-duration attributes sequence the drawing like a storyboard: sky, tenements, the el, the train, and then a two-stroke throw-up (a fat ink outline and a thinner cream stroke over it). All 55 paths are generated in code from a seeded hand-wobble, and the train masks the buildings behind it. Watercolour washes made with feTurbulence fade in as the pen finishes. You can switch between story, delayed, sync and one-by-one, change the timing curve, or scrub the pen by dragging.

npm i vivus
the code that drives it
const L = [
  [[232, 302], [236, 352]],
  [[278, 354], [276, 302], [336, 352], [338, 300]],
  [[384, 300], [384, 354]], [[436, 296], [390, 327], [440, 356]],
];
const letters = L.map((p) => poly(p, 0.8)).join('');
add(letters, { start: 240, dur: 40, w: 30 });
add(letters, { start: 262, dur: 40, w: 21, color: '#e9d9b8' });
// ...
viv = new Vivus(ink, {
  type: state.type,
  duration: state.type === 'oneByOne' ? 300 : 210,
  start: 'manual',
  pathTimingFunction: Vivus[state.timing],
  animTimingFunction: Vivus.EASE_OUT,
}, () => syncWash());
live demo drag the hours · tick extras · hover to re-sketch · try red lamp mode ↗ full window
web componentssvgvanilla

wired-elements

Late-session booking, in pencil

Hand-drawn, sketchy web components rendered with rough.js

wired-elements is a set of Lit web components (inputs, radios, sliders, toggles, combos, cards, dialogs) whose outlines are re-drawn by rough.js every time they render, so no two controls are drawn quite alike. This studio-booking checkout is built only from its tags. The receipt is live, and hovering a control calls wiredRender(true), so its pencil lines re-draw under the cursor.

npm i wired-elements
the code that drives it
import 'wired-elements';

$('#room').addEventListener('selected', e => { state.room = e.detail.selected; update(); });
$('#hours').addEventListener('change', e => { state.hours = e.detail.value; update(); });
document.querySelectorAll('wired-checkbox').forEach(c =>
  c.addEventListener('change', e => { state[c.dataset.k] = e.detail.checked; update(); }));

// pointer: hovering any control makes rough.js re-draw it, so the pencil lines "boil" under your hand
function resketch(list) { list.forEach(el => el.wiredRender && el.wiredRender(true)); }
document.addEventListener('pointerover', e => {
  const el = e.target.closest && e.target.closest('wired-button,wired-checkbox,wired-radio,wired-slider,wired-toggle,wired-input,wired-progress');
  if (el) resketch([el]);
});
site ↗source ↗npm ↗MIT · v3.0.0-rc.6
live demo draw on it · switch architect/artist/cartoonist · swap fills ↗ full window
reactcanvas

Excalidraw

Track map on a whiteboard

The embeddable hand-drawn whiteboard, as a React component

Excalidraw is a complete editor, not just a renderer. Shapes are rough.js-drawn and editable, with hachure, cross-hatch and zigzag fills, a sloppiness setting, and its own handwriting font. Here a song arrangement is pre-drawn as skeleton elements via convertToExcalidrawElements, with fonts served locally through EXCALIDRAW_ASSET_PATH. A locked playhead element is moved at 92 bpm through updateScene. The pill on top rewrites roughness and fill style on every element.

npm i @excalidraw/excalidraw react react-dom
the code that drives it
window.EXCALIDRAW_ASSET_PATH = new URL('../excalidraw-assets/', window.location.href).href;

sk.push({
  type: 'rectangle', id: 'sec' + i, x, y: 100, width: w, height: 92,
  backgroundColor: bg, fillStyle: fill, strokeColor: INK, strokeWidth: 2, roughness: 1,
  label: { text: `${name}\n${bars} bars`, fontSize: bars >= 8 ? 20 : 13, fontFamily: FF },
});
const els = convertToExcalidrawElements(sk, { regenerateIds: false });

// pill: rewrite roughness / fillStyle on every element in one scene update
const els = api.getSceneElements().map(el => {
  if (el.id === 'playhead') return el;
  const p = {};
  if ('roughness' in patch) p.roughness = patch.roughness;
  if (patch.fill && el.type === 'rectangle' && el.id.startsWith('sec') && el.fillStyle !== 'solid') p.fillStyle = patch.fill;
  return Object.keys(p).length ? newElementWith(el, p) : el;
});
api.updateScene({ elements: els, captureUpdate: CaptureUpdateAction.IMMEDIATELY });
live demo drag roughness & bowing · hover a room · click it to change its fill ↗ full window
svgvanillagenerative

Rough.js

Lower-level plan, cyanotype

A tiny graphics library that draws in a sketchy, hand-drawn style

Rough.js turns lines, arcs, curves, paths and polygons into jittered, double-stroked strokes. Its fill engine covers hachure, zigzag, cross-hatch, dots, dashed and zigzag-line. Each room in this cyanotype plan uses a different filler, and doors, dimensions and the console arc are rough primitives emitted as SVG. The roughness and bowing sliders re-render the sheet, and 'boil' re-seeds it six times a second like a pencil test.

npm i roughjs
the code that drives it
import rough from 'roughjs';
const rc = rough.svg(svg);
const opt = (o = {}) => ({ roughness: rough_, bowing, seed: seed + (o.k || 0), stroke: INK, strokeWidth: 1, ...o });

ROOMS.forEach((r, i) => {
  const on = hover === r.id;
  const node = rc.rectangle(r.x, r.y, r.w, r.h, opt({
    fill: on ? GOLD : 'rgba(233,241,255,.55)', fillStyle: r.fill, hachureAngle: r.angle, hachureGap: r.gap,
    fillWeight: on ? 1.6 : 0.9, strokeWidth: 1.6, k: 10 + i * 3, dashOffset: 5, dashGap: 11, zigzagOffset: r.gap * 0.7,
  }));
  node.classList.add('room'); node.dataset.id = r.id;
  g.appendChild(node);
});
// boil: re-seed the hand ~6x/s, like a pencil test on the light box
if (!boil || t - last < 160) return;
last = t; seed = (seed % 3) + 7 + 1; draw();
05 ▣

Retro OS & terminal

9 demos

Whole operating systems as stylesheets, window managers, terminals and console menus. Nostalgia that ships as a CSS file.

live demo pick a window colour · pause the copy · switch Sound tabs · tap Test for sound ↗ full window
cssretrovanillaaudio

7.css

Aero, 11:47 PM

Windows 7 Aero UI in CSS: glass frames, tabs, progress bars and balloons

7.css rebuilds the Aero theme with layered gradients and backdrop-filter glass, plus semantic ARIA tabs and an animated progressbar role. Here the window colour swatches retint every glass frame live, a stem backup copy dialog runs with pause and cancel, and the Sound panel plays a test chime through a Web Audio analyser that drives the level meters.

npm i 7.css
the code that drives it
<div class="window glass" id="w-copy" style="width:372px">
    <div class="title-bar">
      <div class="title-bar-text" id="copy-title">17% complete</div>
      <div class="title-bar-controls">
        <button aria-label="Minimize" data-act="min"></button>
        <button aria-label="Close" data-act="close"></button>
      </div>
    </div>
    <div class="window-body has-space copy">
      <p class="from">Copying 1,204 items from <a href="#" onclick="return false">STEMS (D:)</a> to <a href="#" onclick="return false">BACKUP_2009 (E:)</a></p>
      <p class="instruction instruction-primary" id="copy-pct">17% complete</p>
      <div role="progressbar" class="animate" id="bar" aria-valuemin="0" aria-valuemax="100"><div id="bar-fill" style="width:17%"></div></div>
live demo double-click a drive · open the Tools tab and defragment · click the balloon ↗ full window
cssretrovanillasvg

XP.css

My Computer, 4:47 PM

The Luna-blue Windows XP theme as a drop-in CSS file

XP.css is a fork of 98.css that recreates the Luna title bars, rounded control buttons and tab sheets from markup alone. This scene has a My Computer folder with capacity bars, a drive Properties sheet with a 3D usage pie, a working defrag map and Disk Cleanup, and tray balloon notifications you can click through.

npm i xp.css
the code that drives it
<menu role="tablist">
        <button role="tab" aria-selected="true" aria-controls="pt-gen">General</button>
        <button role="tab" aria-controls="pt-tools">Tools</button>
        <button role="tab" aria-controls="pt-hw">Hardware</button>
        <button role="tab" aria-controls="pt-share">Sharing</button>
      </menu>
      <article role="tabpanel" id="pt-gen">
        <div class="pr-head"><span class="tico lg" id="p-ico" data-ico="hdd"></span><input id="p-label" value="Local Disk"></div>
live demo open the menus · paint the desktop pattern · flip Note Pad pages · double-click Trash ↗ full window
cssretrovanilla

system.css

Finder 6.1: the Black Book

A 1-bit Apple System 6 design system in CSS

system.css turns ARIA roles (menu-bar, menu, menu-item) and classes into System 6 chrome: striped title bars, the pull-down menu bar, alert boxes and dithered controls. This Finder scene has working menus, a Note Pad with eight pages of graffiti notes, a Control Panel whose 8×8 pattern editor repaints the desktop, Puzzle, and an Empty Trash alert. Only the public-domain ChicagoFLF from the package ships.

npm i @sakun/system.css
the code that drives it
<ul role="menu-bar" id="menubar">
  <li role="menu-item" tabindex="0" aria-haspopup="true" class="pear-item"><img data-pix="pear" alt="Pear menu" class="pear">
    <ul role="menu">
      <li role="menu-item"><button data-do="about">About the Finder…</button></li>
      <li role="menu-item" class="divider"><button data-do="cp">Control Panel</button></li>
      <li role="menu-item"><button data-do="np">Note Pad</button></li>
      <li role="menu-item"><button data-do="puzzle">Puzzle</button></li>
    </ul>
  </li>
  <li role="menu-item" tabindex="0" aria-haspopup="true">File
live demo type help · ↑↓ history · tab completes · try matrix ↗ full window
vanillaretrotextascii

xterm.js

LATHE-7 cutting-room console

The terminal emulator inside VS Code, packaged as an embeddable web component with addons.

xterm.js is a real VT emulator. It parses ANSI escapes and handles cursor movement, SGR colours and scrollback, so a fake shell can stream ANSI exactly as a real TTY would. The addon-fit package recomputes cols and rows from the container on every resize, and the neofetch output reflows for narrow tubes. The shell is a record-cutting lathe with help, ls, cat, neofetch and a matrix rain written straight into the terminal buffer. Three phosphor knobs (P1, P3, P4) retheme it live through `term.options.theme`.

npm i @xterm/xterm @xterm/addon-fit
the code that drives it
term.onData(d => {
  if (paused) return;
  if (matrix) { stopMatrix(); return; }
  if (busy) return;
  if (d[0] === '\x1b') {
    if (d === '\x1b[A' || d === '\x1b[B') {
      hIdx = Math.max(0, Math.min(hist.length, hIdx + (d === '\x1b[A' ? -1 : 1)));
      term.write('\b \b'.repeat(line.length)); line = hist[hIdx] || ''; term.write(line);
    }
    return;
  }
  for (const ch of d) {
    const code = ch.charCodeAt(0);
    if (ch === '\r') { const l = line; line = ''; run(l); }
    else if (code === 127) { if (line) { line = line.slice(0, -1); term.write('\b \b'); } }
    else if (code === 3) { line = ''; term.write('^C'); prompt(); }
    else if (code === 12) { term.clear(); }
    else if (ch === '\t') { const m = [...Object.keys(CMDS), ...Object.keys(FS)].find(k => k.startsWith(line.split(' ').pop()) && line.split(' ').pop()); if (m) { const tail = m.slice(line.split(' ').pop().length); line += tail; term.write(tail); } }
    else if (code >= 32 && code !== 127) { line += ch; term.write(ch); }
  }
});
site ↗source ↗npm ↗MIT · v6.0.0 (addon-fit 0.11.0)
live demo switch SCAN / DIVE / BURN · move pointer over cam · tap sonar or hexes ↗ full window
cssretrocanvas

augmented-ui

SALVAGE//DECK drone HUD

Pure-CSS cyberpunk geometry: clipped corners, notched edges, borders and inlays from a single attribute.

augmented-ui generates clip-paths and matching borders from tokens like `tl-clip`, `r-clip-y` and `all-hex` in a `data-augmented-ui` attribute, sized by `--aug-*` custom properties. Geometry is therefore just markup. This salvage-drone deck stores a shape per mode in `data-shape-*` and copies it into the attribute, so SCAN, DIVE and BURN visibly re-cut every panel, bar and hex at runtime. Sonar, hull-cam and vitals readouts update live.

npm i augmented-ui
the code that drives it
/* ---------- augmented-ui: shapes are just attributes, so modes swap geometry live ---------- */
function setMode(m) {
  mode = m; document.body.dataset.mode = m;
  document.querySelectorAll('[data-shape-' + m + ']').forEach(el => el.setAttribute('data-augmented-ui', el.getAttribute('data-shape-' + m)));
  document.querySelectorAll('[data-m]').forEach(b => b.classList.toggle('on', b.dataset.m === m));
  log(MODES[m].msg, true);
}
document.querySelectorAll('[data-m]').forEach(b => b.onclick = () => setMode(b.dataset.m));
site ↗source ↗npm ↗BSD-2-Clause · v2.0.0
live demo click headers to sort · j/k select · x to kill · toggle ON AIR ↗ full window
cssretrotextascii

WebTUI

txtop: transmitter monitor for KSTL 97.4

A CSS library that styles plain HTML like a terminal UI, on a character grid using ch and lh units.

WebTUI uses attribute selectors (`is-="badge"`, `box-="double"`, `is-="switch"`, `is-="progress"`) and sizes everything in ch/lh, so boxes, badges and dialogs snap to a monospace cell grid like a curses app. This is an htop-style night-shift monitor for a radio transmitter. It has CPU, RF and memory meters driven by `--progress-value`, a spectrum drawn with block glyphs inside a box-drawn pane, a sortable process table, and a WebTUI dialog for killing processes.

npm i @webtui/css
the code that drives it
const tgt = [40, 22, 61, 18, 82, 6, 57, 9];
function renderMeters() {
  const air = $('#air').checked;
  CORES.forEach((c, i) => {
    const want = i === 4 ? (air ? 82 : 0) : i === 5 ? (air ? 4 + Math.random() * 4 : 0) : tgt[i] + Math.sin(frameN / (6 + i) + i) * 14 + (Math.random() - .5) * 10;
    c[1] += (Math.max(0, Math.min(100, want)) - c[1]) * .35;
    const el = $('#p' + i); el.style.setProperty('--progress-value', c[1].toFixed(1));
    el.classList.toggle('red', c[1] > 85 || (i === 5 && c[1] > 7));
    $('#mv' + i).textContent = i === 4 || i === 5 ? (c[1] * 2.4).toFixed(0).padStart(3) + 'W' : c[1].toFixed(0).padStart(3) + '%';
  });
}
live demo drag items to sort · tap to inspect · double-tap or USE to consume ↗ full window
reactshadcnretro

8bitcn/ui

The Mossback Inn: pixel RPG inventory

A shadcn-compatible registry of 8-bit components: pixel borders, retro font and game UI blocks such as health bars and dialogue.

8bitcn ships as shadcn registry items, so the components land in your repo as editable TSX built on Radix and Tailwind v4 rather than in a black-box package. Its game-specific blocks (HealthBar, ManaBar, XpBar, Dialogue) sit next to pixel-bordered Card, Tabs, Tooltip and Badge. This demo is a rainy-night inn inventory with hand-keyed 12×12 sprites, drag-to-sort slots, item use that moves the HP and MP bars, and an innkeeper who reacts in the Dialogue block.

npx shadcn@latest add https://8bitcn.com/r/health-bar.json https://8bitcn.com/r/dialogue.json https://8bitcn.com/r/tabs.json
the code that drives it
<CardContent className="flex-1 px-4 pb-2 flex flex-col justify-between gap-2 max-[600px]:hidden min-h-0">
            <div className="relative">
              <div className="flex justify-between text-[8px] retro mb-1.5"><span>HP</span><span>{hp}/60</span></div>
              <HealthBar value={hp / 60 * 100} variant="retro" className="h-3" />
              {pops.map(p => <span key={p.id} className="float retro absolute right-0 -top-2 text-[9px]" style={{ color: p.color, textShadow: '1px 1px 0 #2a1d12' }}>{p.txt}</span>)}
            </div>
            <div><div className="flex justify-between text-[8px] retro mb-1.5"><span>MP</span><span>{mp}/40</span></div><ManaBar value={mp / 40 * 100} variant="retro" className="h-3" /></div>
            <div><div className="flex justify-between text-[8px] retro mb-1.5"><span>XP</span><span>{xp}%</span></div><XpBar value={xp} className="h-3" levelUpMessage="LV UP!" /></div>
            <div className="grid grid-cols-2 gap-x-4 gap-y-0.5 pt-1">
site ↗source ↗npm ↗MIT · vregistry @ 8bitcn.com (Oct 2026)
06 ↯

Motion, physics & tactile

9 demos

Things to fling, squish, tilt and scroll through. Physics engines, morphing and springs that make interfaces feel like objects.

live demo tap pads to write a beat · flick the faders ↗ full window
vanillasvgmotion

anime.js v4

PAD/48 ripple sequencer

A modular JavaScript animation engine — v4 adds draggables, timers, SVG morphing and text scrambling

v4 rebuilt anime.js around ES modules with new primitives: createDraggable (physics-style release springs inside a container), createTimer, grid-aware stagger, svg.morphTo, svg.createDrawable and text scrambleText. A playable 8×6 step sequencer: taps ripple outward via stagger({grid, from}), the LCD morphs a glyph per instrument and line-draws its waveform, and two springy draggable faders set tempo and ripple speed.

npm i animejs
the code that drives it
animate(pads, {
  scale: [{ to: 0.8, duration: 110 }, { to: 1, duration: 650, ease: 'outElastic(1, .45)' }],
  delay: stagger(ripple, { grid: [COLS, ROWS], from: index }),
});
animate(drawables[r], { draw: ['0 0', '0 1', '1 1'], duration: 1100 });
animate('#shape', { d: svg.morphTo(`#s${r}`), duration: 420, ease: 'outBack(1.6)' });
animate(instEl, { innerHTML: scrambleText({ text: NAMES[r], chars: 'blocks' }) });

createDraggable('#k-tempo', {
  container: '#t-tempo', x: false,
  releaseStiffness: 140, releaseDamping: 9,
  onUpdate: self => setTempo(1 - self.progressY),
});
timer = createTimer({ duration: 60000 / bpm / 2, loop: true, onLoop: tick });
live demo drag to rotate · pick a colourway ↗ full window
vanillacanvas3dmotion

Zdog

ZD-87 boombox

A round, flat, designer-friendly pseudo-3D engine for canvas and SVG

Zdog renders 3D models as flat 2D strokes with round caps, no lighting and no meshes, so things look like vector illustrations that happen to rotate. The boombox is built from Box, Cone, Hemisphere, Cylinder and arc-path Shapes; the woofers pump on the beat, the LED meter redraws via updatePath(), and pseudo-3D confetti pours from the cones.

npm i zdog
the code that drives it
const illo = new Zdog.Illustration({
  element: canvas, dragRotate: true, resize: true,
  onResize(w, h) { this.zoom = Math.min(w / 340, h / 300); },
});
new Zdog.Box({ addTo: model, width: 224, height: 122, depth: 58, stroke: 4,
  color: C.body, rearFace: C.side, leftFace: C.side, topFace: C.top });
const cone = new Zdog.Anchor({ addTo: spk });
new Zdog.Cone({ addTo: cone, diameter: 62, length: 16, color: C.cone, rotate: { y: TAU / 2 } });
new Zdog.Hemisphere({ addTo: cone, diameter: 18, color: C.accent, translate: { z: -7 } });
// on every frame
cones.forEach(c => { c.translate.z = 4 * thump; c.scale = 1 + 0.07 * thump; });
illo.updateRenderGraph();
live demo hover or touch-drag a sleeve · pull the wax ↗ full window
vanillacss3dmotion

Atropos

Dollar-bin record sleeves

Touch-friendly 3D parallax hover effects from layered DOM

Atropos tilts an element in CSS 3D and moves each child by its data-atropos-offset, so ordinary HTML layers separate into depth with a moving highlight and shadow. Each sleeve stacks procedural SVG cover art, type, a price tag and a sticker at different offsets; the record sits behind the sleeve and slides out while a card is active.

npm i atropos
the code that drives it
el.innerHTML = `
<div class="atropos-scale"><div class="atropos-rotate">
  <div class="layer" data-atropos-offset="-4"><div class="wax">…</div></div>
  <div class="atropos-inner">
    <div class="cover" data-atropos-offset="-2">${art(r.art)}</div>
    <div class="layer title" data-atropos-offset="6">${r.title}</div>
    <div class="layer sticker" data-atropos-offset="11">…</div>
  </div>
</div></div>`;
Atropos({ el, activeOffset: 40, shadowScale: 1.04, rotateXMax: 16, rotateYMax: 16, rotateTouch: 'scroll-x' });
/* CSS */ .sleeve.atropos-active .wax { transform: translateX(34%) }
live demo scroll or swipe the zine · fling fast on page 04 · chapter buttons + syrup/silk/raw ↗ full window
vanillascrollmotioncss

Lenis

Side B: a pocket zine that scrolls like silk

Smooth-scroll engine from darkroom.engineering that keeps native scroll but interpolates it

Lenis doesn't fake scrolling with transforms. It lerps the real scrollTop of whatever box you hand it, so sticky positioning, anchors and scroll events keep working. Here it drives a nested wrapper/content pair inside the iframe. Its per-frame scroll, progress and velocity feed a riso zine: a record that rotates with scroll distance, a tracklist pinned and read sideways, wildstyle letters that assemble, and type sheared by velocity. Chapter buttons call lenis.scrollTo with a custom easing, and the feel presets swap the lerp factor live.

npm i lenis
the code that drives it
const lenis = new Lenis({
  wrapper: wrap,
  content,
  lerp: 0.1,
  smoothWheel: true,
  syncTouch: true,
  syncTouchLerp: 0.08,
  touchMultiplier: 1.4,
  autoRaf: false,
});
// …
lenis.on('scroll', ({ scroll, progress, velocity }) => {
  root.style.setProperty('--p', progress.toFixed(4));
  root.style.setProperty('--v', clamp(velocity, -40, 40).toFixed(2));

  // cover: plates drift out of register as you leave
  root.style.setProperty('--c', clamp(scroll / wrap.clientHeight).toFixed(3));

  // 01: scroll distance is record rotation
  const spin = sections.spin;
  spin.style.setProperty('--rot', (scroll * 0.55).toFixed(1));
live demo tap a sleeve to pull it · drag a row to reorder · sort by bpm or key ↗ full window
vanillamotioncss

AutoAnimate

Crate & Set: a setlist that rearranges itself

One function call that adds FLIP transitions to any parent whose children are added, removed or moved

AutoAnimate watches a parent element with a MutationObserver and FLIP-animates any child that enters, leaves or moves, with no animation code at the call site. The crate uses the default animation. The setlist passes a custom plugin function that returns its own KeyframeEffect for each add, remove and move, so records slide in like they're pulled from a sleeve and tilt as they reorder. The DOM is keyed by hand (nodes are moved, never recreated), which is all the library needs.

npm i @formkit/auto-animate
the code that drives it
const listCtl = autoAnimate(list, (el, action, oldC, newC) => {
  let kf;
  if (action === 'add') {
    kf = [
      { transform: 'translateX(-70px) rotate(-5deg) scale(.96)', opacity: 0 },
      { transform: 'translateX(6px) rotate(1deg)', opacity: 1, offset: 0.7 },
      { transform: 'none', opacity: 1 },
    ];
    return new KeyframeEffect(el, kf, { duration: 420, easing: 'cubic-bezier(.3,1.4,.5,1)' });
  }
  if (action === 'remove') {
    kf = [
      { transform: 'none', opacity: 1 },
      { transform: 'translateX(-90px) rotate(-8deg)', opacity: 0 },
    ];
    return new KeyframeEffect(el, kf, { duration: 300, easing: 'ease-in' });
  }
  const dx = oldC.left - newC.left, dy = oldC.top - newC.top;
  const tilt = Math.sign(dy) * Math.min(3, Math.abs(dy) / 18);
  kf = [
live demo tap a sticker to re-cut it · drag to peel and move · wiggle speed · ink swatches ↗ full window
vanillacanvasgenerativemotion

blobs

Goop: a sticker sheet that keeps wobbling

g-harel's seeded generator for organic blob paths, with a keyframe animation engine

blobs builds a smooth closed bezier from a seed, a point count and a randomness value, so the same seed always gives the same shape. Its v2/animate module interpolates between shapes, including different point counts, with elastic timing functions and a wiggle preset. Each sticker is a live canvasPath() animation driven by a clock the demo owns, so pausing freezes every blob mid-wobble. The Path2D it returns is reused for the die-cut border, the pattern clip and pointer hit-testing.

npm i blobs
the code that drives it
function opts(s) { return { seed: s.seed, extraPoints: s.pts, randomness: s.rnd, size: 200 }; }
function wiggle(s) { wigglePreset(s.anim, opts(s), { offsetX: 0, offsetY: 0 }, { speed }); }
function recut(s, ms = 900) {
  s.seed = 'cut' + seedN++;
  s.pts = 3 + ((Math.random() * 4) | 0);
  s.rnd = 2.5 + Math.random() * 2.5;
  s.anim.transition({ duration: ms, timingFunction: 'elasticEnd0', blobOptions: opts(s), callback: () => wiggle(s) });
}
site ↗source ↗npm ↗MIT · v2.3.0-beta.2
live demo drag and flick cards left/right · spring: tight/loose/wobbly · re-deal ↗ full window
reactmotionphysics

use-gesture + react-spring

Rhyme deck: flick cards to keep or burn

Pointer gestures that report velocity and direction, driving physics-based springs

@use-gesture/react's useDrag reports movement, velocity and direction for each card. react-spring's useSprings animates all nine cards from a single imperative API, with no keyframes and no durations, just tension and friction. A fast flick (velocity > 0.25) or a long drag throws a card off the table and files the word into the verse. Anything less springs it back. The spring presets swap the physics live, and the pause message freezes every spring mid-flight with api.pause().

npm i @use-gesture/react @react-spring/web
the code that drives it
const bind = useDrag(({ args: [index], active, movement: [mx, my], direction: [xDir], velocity: [vx], tap }) => {
    if (tap) return;
    const fling = vx > 0.25 || Math.abs(mx) > Math.min(innerWidth * 0.28, 220);
    const dir = (Math.abs(mx) > 10 ? Math.sign(mx) : xDir) || 1;
    if (!active && fling && !gone.has(index)) settle(index, dir);
    setLean(active ? Math.max(-1, Math.min(1, mx / 160)) : 0);
    api.start((i) => {
      if (i !== index) return;
      const out = gone.has(index);
      const x = out ? (innerWidth + 260) * dir : active ? mx : 0;
      const y = out ? my * 1.2 - 60 : active ? my * 0.6 : rest(i).y;
      const rot = mx / 90 + (out ? dir * 12 * Math.max(1, vx) : 0) + (active ? 0 : rest(i).rot * (out ? 0 : 1));
      const base = FEELS[feelRef.current];
      return { x, y, rot, scale: active ? 1.07 : 1, delay: undefined, config: active ? { tension: 900, friction: 48 } : out ? { tension: 180, friction: 40 } : base };
    });
  }, { filterTaps: true });

  const throwTop = (dir) => {
    let top = -1; for (let i = CARDS.length - 1; i >= 0; i--) if (!gone.has(i)) { top = i; break; }
site ↗source ↗npm ↗MIT · v@use-gesture/react 10.3.1 · @react-spring/web 10.1.2
07 ✧

Visual effects

9 demos

Particles, glitch, film grain, glass and cursor tricks, used in service of a composed scene rather than as a gimmick.

live demo hit Drop it · tap the paper for a pinch · switch Cannons / Rain / Press ↗ full window
vanillacanvasmotionphysics

canvas-confetti

Release-day product card with record-shaped confetti

Tiny dependency-free confetti cannon that can rasterise any SVG path or glyph into a particle shape

canvas-confetti is usually seen firing default squares. Its shapeFromPath and shapeFromText helpers turn an SVG path or a glyph into a reusable particle. The vinyl record is two arcs wound in opposite directions, so the nonzero fill leaves a real spindle hole. It sits alongside a 45 rpm adapter, a bolt, a cassette and '45' and ♪ glyphs set in Fraunces. Everything fires into a private canvas made with confetti.create() that sits between the sleeve and the card copy, so the copy stays readable.

npm i canvas-confetti
the code that drives it
const fire = confetti.create(fxCanvas, { resize: true, useWorker: false });
// outer circle clockwise + spindle hole counter-clockwise: nonzero fill leaves a hole
const ring = (r, cw) => `M ${-r} 0 A ${r} ${r} 0 1 ${cw} ${r} 0 A ${r} ${r} 0 1 ${cw} ${-r} 0 Z`;
const record = confetti.shapeFromPath({ path: `${ring(10, 1)} ${ring(2.2, 0)}` });
const adapter = confetti.shapeFromPath({
  path: 'M0 -10 L3 -3 L10 -1 L4 3 L6 10 L0 6 L-6 10 L-4 3 L-10 -1 L-3 -3 Z',
});
const glyph = confetti.shapeFromText({ text: '45', scalar: 2.4, color: '#1f3fbf', fontFamily: 'Fraunces' });

function burst(opts) {
  const base = { colors: PALETTE, disableForReducedMotion: true, ticks: 260 };
  fire({ ...base, shapes: [record, record, adapter, bolt, tape], scalar: 1.25, ...opts });
}
burst({ particleCount: 60, angle: 58, spread: 52, startVelocity: 62, origin: { x: 0, y: 1 } });
live demo switch Tremor / Slice / Tear · set trigger to Loop, Hover or Click ↗ full window
vanillacssmotionretrotext

PowerGlitch

Dead Air: a film title card under interference

Glitches any DOM element by stacking clipped, displaced clones animated with the Web Animations API

PowerGlitch doesn't use a shader or canvas. It wraps the element in two grid containers, stacks N clones on top, and animates clip-path, translate and CSS filters on each clone with the Web Animations API. The whole title card glitches as one unit: illustration, scanlines, title and billing block. The tower-block plate is drawn procedurally and handed to an <img> as a data URL, so every clone carries a real src. glitchTimeSpan keeps the frame calm for most of each loop.

npm i powerglitch
the code that drives it
plate.src = drawPlate(); // a data-URL <img>: every cloned layer inherits a real src

const FX = {
  tremor: { shake: { velocity: 18, amplitudeX: 0.03, amplitudeY: 0.02 },
            slice: { count: 5, velocity: 16, minHeight: 0.02, maxHeight: 0.1, hueRotate: false }, pulse: false },
  tear:   { shake: { velocity: 10, amplitudeX: 0.12, amplitudeY: 0 },
            slice: { count: 8, velocity: 12, minHeight: 0.04, maxHeight: 0.22,
                     cssFilters: 'invert(1) sepia(1) saturate(6) hue-rotate(-30deg)' }, pulse: { scale: 1.02 } },
};
ctl = PowerGlitch.glitch(card, {
  playMode: mode,            // 'always' | 'hover' | 'click'
  hideOverflow: true,
  timing: { duration: 3200, iterations: Infinity },
  glitchTimeSpan: { start: 0.42, end: 0.68 },   // calm frame, interference mid-loop
  ...FX[fx],
});
live demo move over the wall · pick a trail on the line map ↗ full window
vanillacanvascursorretro

cursor-effects

Marsh Lane: cursor trails on a tiled platform wall

Tim Holman's collection of 90s-style cursor trails, each a self-contained canvas you can scope to one element

cursor-effects ships a dozen old-web cursor trails as plain functions that return { destroy }. Passed an element, each one mounts its own absolutely positioned canvas inside it. This demo swaps five of them on a station wall built from an SVG running-bond tile. characterCursor uses custom tag letters and gravity drips, rainbowCursor becomes a three-nib marker, then fairyDustCursor, a textFlag departure line and ghostCursor stamping a locally drawn ticket. When idle, a phantom hand dispatches synthetic mousemove events.

npm i cursor-effects
the code that drives it
import { characterCursor, rainbowCursor, fairyDustCursor, textFlag, ghostCursor } from 'cursor-effects';

const TRAILS = {
  tag: () => characterCursor({
    element: wall,
    characters: ['K', 'R', 'A', 'Z', 'E', 'K', 'R', '!'],
    font: '800 34px Archivo',
    colors: ['#e2463d', '#f3eedf', '#f0c84a'],
    initialCharacterVelocityFunction: () => ({ x: (Math.random() - 0.5) * 3, y: -Math.random() * 2.2 }),
    characterVelocityChangeFunctions: { x_func: () => 0, y_func: (age) => (age < 12 ? 0.05 : 0.14) },
  }),
  marker: () => rainbowCursor({ element: wall, length: 44, colors: ['#e2463d', '#f3eedf', '#f0c84a'], size: 6 }),
  flag: () => textFlag({ element: wall, text: 'NEXT TRAIN TERMINATES HERE · ALL CHANGE', color: '#f0c84a', font: 'Archivo' }),
  ticket: () => ghostCursor({ element: wall, image: ticketImage(), randomDelay: true, minDelay: 30, maxDelay: 90 }),
};
function mount() { fx?.destroy?.(); fx = paused ? null : TRAILS[current](); }
live demo move over the pattern · arcs / dots / shards · ⚂ re-roll for a new seed ↗ full window
web componentscssgenerative

css-doodle

Block Party riso poster

A web component that turns a CSS-like mini-language into generative grid patterns

In css-doodle, every cell of a <css-doodle> grid is a real element styled by one rule set. That rule set gets per-cell functions such as @p (pick), @r (random), @x/@y and @random() selectors, and the seed attribute makes any roll reproducible. This poster swaps three doodle programs: Truchet arcs, sine-modulated halftone dots and overprinted triangle shards. JS writes the pointer's cell coordinate into --mx/--my on the host, and each cell computes hypot(@x - mx, @y - my) in pure CSS. Arcs near the cursor flip 90° and re-route the paths; dots shrink and shards swing.

npm i css-doodle
the code that drives it
truchet: ({ cols, rows, w, h, cs }) => `
  @grid: ${cols}x${rows} / ${w}px ${h}px;
  --c: ${INK.blue};
  --d: hypot(calc(@x - var(--mx)), calc(@y - var(--my)));
  background:
    radial-gradient(circle at 0 0, transparent ${cs * .34}px, var(--c) ${cs * .34}px ${cs * .66}px, transparent ${cs * .66}px),
    radial-gradient(circle at 100% 100%, transparent ${cs * .34}px, var(--c) ${cs * .34}px ${cs * .66}px, transparent ${cs * .66}px);
  rotate: calc(@p(0, 90) * 1deg + clamp(0, (2.4 - var(--d)) * 100, 1) * 90deg);
  @random(.16) { --c: ${INK.pink}; }
  mix-blend-mode: multiply;
  transition: rotate .55s cubic-bezier(.2, .9, .25, 1.2);
  @random(.06) { --c: ${INK.sun}; }
`,
// re-roll
seed = 'riso-' + Math.floor(Math.random() * 9973);
doodle.setAttribute('seed', seed);
doodle.update(PRESETS[preset](geom));
live demo move near the card to stretch the glass · watch the ticker bend through the top bezel · switch refraction mode under the card ↗ full window
reactsvgcssmotion

liquid-glass-react

ZEST FM: liquid glass over a rooftop broadcast

Apple-style Liquid Glass for React, built from SVG displacement filters and backdrop-filter

liquid-glass-react does no WebGL at all. It builds an SVG filter per component: a displacement-map feImage, three feDisplacementMap passes split per RGB channel for edge aberration, and an edge mask. That filter is applied to a backdrop-filter layer, so it bends whatever DOM sits underneath. Here the ZEST FM wordmark stays whole and the player card is parked so only its thick top bezel straddles the band's foot: the moving fine-print ticker and lime rule pass under it and arc through the lens. Elasticity stretches the glass toward the pointer, and the four refraction modes (including the generated 'shader' map) switch live.

npm i liquid-glass-react
the code that drives it
const common = { mouseContainer: stage, mode, aberrationIntensity: 0.2 };
// only the thick top bezel crosses the band: the ticker and lime rule arc through it
const overlap = L.strip + (L.small ? 6 : 10);
const cardCY = Math.min(L.bottom - overlap + outerH / 2, vp.H - (L.small ? 54 : 34) - outerH / 2);

<LiquidGlass {...common} className="lens" displacementScale={30} blurAmount={0} saturation={180} elasticity={0.12} cornerRadius={30}
  padding={`${pad}px ${side}px ${side}px`} style={{ position: 'absolute', top: `${cardCY}px`, left: `${cardCX}px` }}>
  <article ref={cardRef} className="card" style={{ width: cardW }}>
    <h1>{tr.t}</h1>
    <p className="artist">{tr.a}</p>
    <div className="bar"><span style={{ width: `${(pos / tr.len) * 100}%` }} /></div>
    <div className="transport" role="group" aria-label="transport">
      <button aria-label="previous" onClick={() => skip(-1)}><Icon d="M6 5h2.4v14H6zM19 5v14L9.5 12z" /></button>
      {/* play/pause + next */}
    </div>
  </article>
</LiquidGlass>
live demo drag across the sleeve to bend the tide · tap for a new print · rust / moss / night · ■ stop slows the tide ↗ full window
webglgradientshadervanilla

@mesh-gradient/core

Soft Concrete: Low Tide Hymns sleeve

A SwiftUI-style animated mesh gradient for the web: a deformed WebGL plane with no dependencies

@mesh-gradient/core renders four colours as noise-driven wave layers on a vertex-deformed WebGL mesh. It also exposes hot-patch setters (setColors, setFrequency, setSeed, setAnimationSpeed) that retune the running shader without tearing down the GL context. Here the mesh is the whole frame: a full-bleed record sleeve with a liner card on top, and a second tiny instance is the spinning label sticker. Pointer position feeds setFrequency to stretch the noise field, a tap re-seeds the print, stop/play drops the animation speed, and the three 'pressings' cross-fade palettes via update({ transition }).

npm i @mesh-gradient/core
the code that drives it
const sleeve = new MeshGradient();
sleeve.init(document.getElementById('mesh'), {
  colors: PRESSINGS[0].colors, seed: 14, animationSpeed: SPEED.on, frequency: BASE,
  pauseOnOutsideViewport: false, reducedMotion: 'ignore', appearance: 'smooth', appearanceDuration: 900, targetFps: 60,
});
// pointer over the sleeve bends the noise field: x → horizontal frequency, y → vertical
function bend(e) {
  const u = e.clientX / innerWidth, v = e.clientY / innerHeight;
  target = { x: 0.00003 + u * 0.0003, y: 0.00008 + (1 - v) * 0.00055 };
  if (!raf) raf = requestAnimationFrame(ease);
}
// in ease(): sleeve.setFrequency({ x: fq.x, y: fq.y });
sleeveEl.addEventListener('click', () => sleeve.setSeed(Math.floor(Math.random() * 1000)));
sleeve.setAnimationSpeed(on ? SPEED.on : SPEED.off);
sleeve.update({ colors: p.colors, transition: true, transitionDuration: 700 });
live demo hover or drag a card · switch foil: prism / galaxy / cracked ice ↗ full window
vanillacss3dcursormotion

vanilla-tilt

Sample Pack holo trading cards

A tiny dependency-free parallax tilt with optional glare, driven by mouse or gyroscope

vanilla-tilt sets a perspective transform from the pointer and can inject its own glare layer. More usefully, it emits a tiltChange event with percentageX/Y on every update. The demo pipes that feed into CSS custom properties, which slide three foil recipes across each card's art window and frame. Prism is a dodge-blended spectrum, galaxy is star dots over a conic nebula, and cracked ice is a generated triangulation multiplied by a moving rainbow. Touch drags are forwarded as synthetic mouse events, because the library only listens to the mouse.

npm i vanilla-tilt
the code that drives it
VanillaTilt.init(card, {
  max: 18, perspective: 900, speed: 500, scale: 1.06, glare: true, 'max-glare': 0.55,
  startX: [-6, 0, 6][i], startY: [4, 0, 4][i], 'reset-to-start': true, easing: 'cubic-bezier(.2,.8,.2,1)',
});
card.addEventListener('tiltChange', e => {
  const { percentageX: x, percentageY: y } = e.detail;
  card.style.setProperty('--px', `${x}%`);
  card.style.setProperty('--py', `${y}%`);
  card.style.setProperty('--fx', (x / 100).toFixed(3));
  card.style.setProperty('--fy', (y / 100).toFixed(3));
});
/* css */
.foil { background: var(--foil); background-size: 260% 260%; background-position: var(--px) var(--py);
  mix-blend-mode: color-dodge; mask-image: radial-gradient(farthest-corner circle at var(--px) var(--py), #000 0%, rgba(0,0,0,.35) 45%, rgba(0,0,0,.08) 80%); }
08 ⚛

Component registries & kits

9 demos

Copy-in component registries with a strong opinion. Each demo is a small product UI composed from one kit.

live demo toggle per hour / lockout · hover the kicker · hold a slot ↗ full window
reactshadcnmotioncanvastext

Magic UI

Room 4 — studio booking rates

150+ animated marketing components shipped as a shadcn registry (Tailwind + motion)

A pricing block built from seven Magic UI pieces: a canvas FlickeringGrid as an LED-meter backdrop, HyperText scramble kicker, rough-notation Highlighter marks on the serif headline, spring-driven NumberTicker prices that re-count when you switch billing, two counter-phased BorderBeams on the featured tier, an AnimatedList booking feed and canvas-confetti fired from the clicked button's position.

npx shadcn@latest add https://magicui.design/r/flickering-grid.json https://magicui.design/r/hyper-text.json https://magicui.design/r/number-ticker.json https://magicui.design/r/border-beam.json https://magicui.design/r/highlighter.json https://magicui.design/r/animated-list.json https://magicui.design/r/confetti.json
the code that drives it
<article key={room.id} className={"card" + (room.featured ? " featured" : "")}>
  {room.featured && !paused && (
    <>
      <BorderBeam size={90} duration={7} colorFrom="#ffb000" colorTo="#b4231c" borderWidth={2} />
      <BorderBeam size={90} duration={7} delay={3.5} colorFrom="#86a37f" colorTo="#ffb000" borderWidth={2} />
    </>
  )}
  {room.featured && <span className="flag">most booked</span>}
  <h2>{room.name}</h2>
  <p className="note">{room.note}</p>
  <div className="price">
    <span className="cur">$</span>
    <NumberTicker value={lockout ? room.lock : room.hour} startValue={0} className="num" />
    <span className="per">/hr{lockout ? " · ×10" : ""}</span>
  </div>
  <ul>
    {room.gear.map((g) => <li key={g}>{g}</li>)}
  </ul>
site ↗source ↗npm ↗MIT · vregistry snapshot 2026-10-22 (no npm version); deps motion 12.43.0, rough-notation 0.5.1, canvas-confetti 1.9.4
live demo tap pads for sound · click cells to edit the pattern · ± bpm · switch tabs ↗ full window
reactshadcnmotionaudio

Cult UI

Chop Shop — pocket sampler app shell

Skeuomorphic, motion-heavy shadcn components: textured buttons, dynamic islands, tactile cards

Cult UI leans tactile, with nested-border TextureCards, bevelled TextureButtons and an iOS-style DynamicIsland driven by size presets and a spring reducer. Here they run in their dark skin as the rubber deck of a candy-paint sampler: the island morphs from compact to long to show the pad you hit, DirectionAwareTabs slide pads, chops and fx in from the side you clicked, and RollingNumber springs the BPM. Pads are coloured by bank, and sound is a small WebAudio synth with no samples that starts only after you tap a pad.

npx shadcn@latest add https://cult-ui.com/r/dynamic-island.json https://cult-ui.com/r/direction-aware-tabs.json https://cult-ui.com/r/texture-card.json https://cult-ui.com/r/texture-button.json https://cult-ui.com/r/rolling-number.json
the code that drives it
function Island({ last, playing, bar, sound }) {
  const { state, setSize } = useDynamicIslandSize();
  useEffect(() => { setSize(last ? "long" : "compact"); }, [last?.stamp]);
  useEffect(() => {
    if (!last) return;
    const t = setTimeout(() => setSize("compact"), 1100);
    return () => clearTimeout(t);
  }, [last?.stamp]);
  return (
    <DynamicIsland id="now">
      {state.size === "long" && last ? (
        <DynamicContainer className="flex h-full w-full items-center gap-3 px-5">
          <span className="pad-dot" style={{ background: `var(--c-${last.voice})` }} />
          <div className="text-left">
            <DynamicTitle className="island-t">{last.id} · {last.name}</DynamicTitle>
            <DynamicDescription className="island-d">{last.voice} voice · synth, no sample</DynamicDescription>
          </div>
        </DynamicContainer>
      ) : (
        <DynamicContainer className="flex h-full w-full items-center justify-between px-4">
site ↗source ↗npm ↗MIT · vregistry snapshot 2026-10-22 (no npm version); deps motion 12.43.0, react-use-measure 2.1.7
live demo move over the wall to torch it · pick a colourway · nudge the RSVP button ↗ full window
reactshadcnmotionsvgcursortext

Motion Primitives

Paint the Whole Yard — event hero

ibelick's small, composable motion building blocks for React, distributed as a shadcn registry

Motion Primitives ships low-level pieces, not finished sections. This hero composes nine of them: TextEffect and TextScramble reveal the copy, SlidingNumber rolls a live countdown, Magnetic and BorderTrail make the RSVP pill, Tilt and Spotlight turn the wall into a torch-lit surface, AnimatedBackground slides the colourway picker, and InfiniteSlider runs the writers' band. The piece itself is a procedural SVG with stacked offset copies for the extrusion.

npx shadcn@latest add https://motion-primitives.com/c/text-effect.json https://motion-primitives.com/c/text-scramble.json https://motion-primitives.com/c/infinite-slider.json https://motion-primitives.com/c/magnetic.json https://motion-primitives.com/c/tilt.json https://motion-primitives.com/c/spotlight.json https://motion-primitives.com/c/sliding-number.json https://motion-primitives.com/c/animated-background.json https://motion-primitives.com/c/border-trail.json
the code that drives it
<Tilt rotationFactor={7} isRevese springOptions={{ stiffness: 160, damping: 18 }} className="wall-tilt">
    <div className="wall">
      <Spotlight size={360} className="spot" springOptions={{ bounce: 0.1, stiffness: 120, damping: 20 }} />
      <Piece wall={wall} />
      <span className="wall-label">wall 3 of 7 · torch on it</span>
    </div>
  </Tilt>
  <div className="picker">
    <AnimatedBackground
      defaultValue="chrome"
      className="picker-bg"
      transition={{ type: "spring", bounce: 0.25, duration: 0.45 }}
      onValueChange={(id) => id && setWall(id)}
    >
      {Object.keys(WALLS).map((k) => (
        <button key={k} data-id={k} type="button" className="pick">{k}</button>
      ))}
    </AnimatedBackground>
  </div>
</section>
site ↗source ↗npm ↗MIT · vregistry snapshot 2026-10-22 (no npm version); deps motion 12.43.0, react-use-measure 2.1.7
live demo hover the LED board to freeze it, drag to tag it · Drop station ID · swap LED colours ↗ full window
reactshadcncanvasretro

cult/ui

WKTZ 88.3: pirate radio desk

Opinionated shadcn registry of skeuomorphic, tactile React components.

cult/ui leans tactile. TextureCard stacks four hairline bezels to fake machined plastic, TextureButton does the same for keys, and LightBoard is a canvas dot-matrix marquee you can draw on with the pointer. This desk uses the LightBoard as a station sign you can tag, with a rolling listener count, an on-air timer and swappable LED colourways.

npx shadcn@latest add https://www.cult-ui.com/r/lightboard.json https://www.cult-ui.com/r/texture-card.json https://www.cult-ui.com/r/texture-button.json https://www.cult-ui.com/r/rolling-number.json https://www.cult-ui.com/r/timer.json
the code that drives it
<LightBoard
              key={led}
              text={text}
              rows={15}
              lightSize={6}
              gap={2}
              updateInterval={45}
              colors={LEDS[led]}
              disableDrawing={false}
              controlledDrawState="2"
              controlledHoverState={hover}
              paused={paused}
            />
site ↗source ↗MIT · vregistry @ nolly-studio/cult-ui 7f031dc (fetched 2026-10-22); motion 14.6.0
live demo switch tabs · drag the sliders · tick paint colours · sign the sheet · try the palette dots ↗ full window
reactshadcncss

neobrutalism components

Wall Jam: a legal-wall sign-up sheet

shadcn-compatible neobrutalist component set on Base UI: hard shadows, thick ink, flat fills.

The kit's whole look runs on a handful of CSS variables: --main, --background, --border and a hard --shadow. Buttons travel into their own shadow on hover. This sign-up sheet composes tabs, sliders, switches, checkboxes, progress, badges and two marquees into a poster-like form. The palette chips swap in the kit's own duotone registry styles at runtime.

npx shadcn@latest add https://www.neobrutalism.dev/r/button.json https://www.neobrutalism.dev/r/card.json https://www.neobrutalism.dev/r/tabs.json https://www.neobrutalism.dev/r/slider.json https://www.neobrutalism.dev/r/marquee.json
the code that drives it
<TabsContent value="paint" className="space-y-3 pt-2">
                <div className="grid grid-cols-2 gap-x-4 gap-y-2.5">
                  {COLOURS.map((c) => (
                    <label key={c.id} className="flex cursor-pointer items-center gap-2.5 text-sm">
                      <Checkbox checked={cols.includes(c.id)} onCheckedChange={() => toggleCol(c.id)} />
                      <span className="size-4 rounded-full border-2 border-border" style={{ background: c.hex }} />
                      {c.name}
                    </label>
                  ))}
                </div>
                <div>
                  <div className="mb-2 flex items-baseline justify-between text-sm">
site ↗source ↗MIT · vregistry @ ekmas/neobrutalism-components 3306a80 (fetched 2026-10-22); @base-ui/react 1.9.0
live demo ⌘K or / for the palette · B bounce · S crate · N notes · [ ] tempo · drag drawers to close ↗ full window
reactmotion

cmdk + sonner + vaul

mixdown: a keyboard-first session shell

Three small headless primitives by Paco Coursey and Emil Kowalski: command menu, toasts, drawer.

cmdk is an unstyled, fuzzy-ranked command menu that you style through data attributes. sonner gives stacking toasts with promise states, and vaul gives drag-to-dismiss drawers with real momentum. Here they make one keyboard-driven producer app: the palette bounces stems (promise toast), sends versions, nudges tempo, and opens a bottom sample crate or a right-side notes drawer.

npm i cmdk sonner vaul
the code that drives it
<Command label="Mixdown command palette" loop>
              <Command.Input ref={inputRef} value={search} onValueChange={setSearch} placeholder={innerWidth < 500 ? "Type a command…" : "Type a command… try “bounce”, “crate”, “tempo”"} />
              <Command.List>
                <Command.Empty>Nothing in the crate called that.</Command.Empty>
                <Command.Group heading="Session">
                  <Command.Item onSelect={run(actions.bounce)} keywords={["export", "render"]}><i className="w-4 text-center not-italic text-acid">⤓</i>Bounce stems<kbd>B</kbd></Command.Item>
                  <Command.Item onSelect={run(actions.send)} keywords={["share", "rapper"]}><i className="w-4 text-center not-italic text-acid">↗</i>Send this version to Rae<kbd>R</kbd></Command.Item>
                  <Command.Item onSelect={run(actions.nudgeUp)} keywords={["bpm", "faster"]}><i className="w-4 text-center not-italic text-acid">+</i>Tempo up 2 BPM<kbd>]</kbd></Command.Item>
                  <Command.Item onSelect={run(actions.nudgeDown)} keywords={["bpm", "slower"]}><i className="w-4 text-center not-italic text-acid">-</i>Tempo down 2 BPM<kbd>[</kbd></Command.Item>
                </Command.Group>
site ↗source ↗npm ↗MIT · vcmdk 1.1.1 · sonner 2.0.8 · vaul 1.1.2
live demo hover or drag along the dock · tap any icon to pin it to the stage ↗ full window
reactshadcnsvgmotion

lucide-animated (pqoqubbw/icons)

Night Shift: an icon-driven app shell

Lucide icons rebuilt as Motion components, each with its own hand-tuned micro-animation, installed one by one.

Each icon is its own copy-in file with bespoke Motion variants: the bell swings, the disc spins, the flame redraws its stroke. Each also exposes an imperative startAnimation()/stopAnimation() handle, so code can drive it as well as hover. Here 25 of them make up the whole interface: a tool rail, a status tray, a magnifying dock with a timed ripple, and a poster-scale stage that loops the pinned icon's own animation at 300px.

npx shadcn@latest add https://lucide-animated.com/r/flame.json https://lucide-animated.com/r/disc-3.json https://lucide-animated.com/r/spray-can.json
the code that drives it
// Idle sweep: a ripple of animations runs down the dock every few seconds.
  useEffect(() => {
    if (paused) return;
    let timers = [];
    const sweep = () => {
      dockRefs.forEach((r, i) => {
        timers.push(setTimeout(() => r.current?.startAnimation(), i * 90));
        timers.push(setTimeout(() => r.current?.stopAnimation(), i * 90 + 900));
      });
    };
    sweep();
    const id = setInterval(sweep, 5200);
    return () => { clearInterval(id); timers.forEach(clearTimeout); dockRefs.forEach((r) => r.current?.stopAnimation()); };
site ↗source ↗MIT · vregistry @ pqoqubbw/icons 4662fff (fetched 2026-10-22); motion 14.6.0
09 ⌁

Sound & creative coding

9 demos

Synths, sequencers, knobs and sketches. Instruments you can actually play, and creative-coding tools with a point of view.

live demo drag knobs (shift = fine) · play the keys or Z S X D C V… · hold two keys for glide ↗ full window
audioweb componentssvg

webaudio-controls

Mudhoney SB-1 sub bass

Knob, slider, switch and keyboard web components for audio apps

g200kg's webaudio-controls are custom elements (<webaudio-knob>, <webaudio-slider>, <webaudio-switch>, <webaudio-keyboard>) that behave like plugin GUIs: drag, wheel, shift for fine, ctrl/cmd-click to reset, value tooltips. Their unusual trick is filmstrip skins: every knob here is a 101-frame SVG sprite sheet generated in code, the same format VST knob galleries use. They drive a raw WebAudio mono bass (saw/square plus sub, tanh drive, resonant lowpass with envelope, glide) with last-note priority.

npm install github:g200kg/webaudio-controls
the code that drives it
// options.js: a knob skin is just a vertical sprite sheet
for (let i = 0; i < 101; i++) {
  const y = i * 128, a = -135 + (270 * i) / 100;
  svg += `${ticks(y, i)}<g transform="rotate(${a} 64 ${y + 64})">…</g>`;
}
knob.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svg));
knob.setAttribute('sprites', '100');

// main.js: components are plain DOM, values flow straight to AudioParams
import '@g200kg/webaudio-controls';
cutoff.addEventListener('input', () => filter.frequency.setTargetAtTime(+cutoff.value, ctx.currentTime, 0.02));
kb.addEventListener('change', (e) => e.note[0] ? noteOn(e.note[1]) : noteOff(e.note[1]));
site ↗source ↗npm ↗Apache-2.0 · v2.1.2 (github g200kg/webaudio-controls@282610a)
live demo drag anywhere: left/right = dull/bright, up/down = restless/calm · pick a sea area ↗ full window
audiogenerativecanvas

Elementary Audio

Shipping forecast for nobody

Write DSP as pure functions of state; a WASM engine diffs and renders the graph

Elementary treats audio like React treats UI: you describe the whole signal graph as a function of state, call render() again whenever state changes, and the WASM runtime (inside an AudioWorklet, shipped inline as a blob) reconciles only what changed. Keyed el.const nodes make every drag a cheap property update. The drone is five detuned blep-saw voices through breathing lowpasses, pink-noise wind and feedback delays; every seven seconds one voice wanders to a new scale tone, gliding via el.smooth. An el.scope node streams the stereo output back as a Lissajous figure.

npm install @elemaudio/core @elemaudio/web-renderer
the code that drives it
function voice(i) {
  const f = el.smooth(el.tau2pole(2.8), el.const({ key: `f${i}`, value: hz(S.root + S.notes[i]) }));
  const osc = el.add(el.blepsaw(f), el.mul(0.6, el.bleptriangle(el.mul(f, 1.0035))));
  const lfo = el.add(0.5, el.mul(0.5, el.cycle(el.const({ key: `lr${i}`, value: lfoRate(i) }))));
  const fc = el.add(60, el.mul(el.sm(el.const({ key: 'cut', value: S.cut })), el.add(0.35, el.mul(0.65, lfo))));
  return el.mul(el.lowpass(fc, 1.3, osc), el.mul(0.11, el.add(0.2, el.mul(0.8, lfo))));
}

// state changed? describe the whole graph again; Elementary diffs it
function render() { core.render(...graph()); }
core.on('scope', (e) => { scopeL = e.data[0]; scopeR = e.data[1]; });
site ↗source ↗npm ↗MIT · vcore 4.0.1 / web-renderer 4.0.3
live demo hit pads or 1-4 Q-R A-F Z-V · drag parameter rows · mutate, random, copy ↗ full window
audioretrocanvas

ZzFX

Blip Depot sound-effects board

A tiny sound-effect synth where every sound is an array of 20 numbers

ZzFX (Zuper Zmall Zound Zynth) from the js13k scene renders a whole effect (envelope, shape, slide, pitch jump, repeat, noise, FM, bitcrush, delay, tremolo, biquad filter) from one flat numeric array, so a sound is small enough to paste into a tweet. Each pad's waveform is drawn from ZZFX.buildSamples, the same buffer it plays. Drag any parameter row to bend the selected sound, mutate or randomize it, then copy the trimmed zzfx(...[…]) call.

npm install zzfx
the code that drives it
import { ZZFX } from 'zzfx';
ZZFX.volume = 0.22;

const BANK = [
  ['COIN',     [1, .05, 1675, , .06, .24, 1, 1.82, , , 837, .06]],
  ['808 DROP', [1.4, .01, 150, .01, .3, .7, 0, 1, -1.6, , , , , , , , , 1, .2]],
  ['SPRAY',    [.7, .05, 1400, .05, .35, .2, 4, 0, , , , , , 3.5, , , , .5, , , 2600]],
  // …
];

// the pad art is the sound: draw the exact buffer ZzFX will play
drawWave(canvas, ZZFX.buildSamples(...params));
ZZFX.play(...params);
live demo drag to spray · bend the psi curve · pick a can · sound toggle adds hiss ↗ full window
vanillacanvasgenerativeaudio

Tweakpane + Essentials

Wet Wall: flow-field spray study

A compact, themeable parameter pane for creative coding, with plugin blades

Tweakpane's Essentials plugin adds blades that most GUI kits lack: an interval slider, a cubic-bezier editor, a radio grid, a button grid and a live FPS graph. Here the bezier blade is not decoration. Its curve is sampled every frame as the nozzle-pressure profile of each of 190 paint streams following a sine flow field over procedurally speckled concrete. The pane is re-skinned entirely through --tp-* CSS variables.

npm i tweakpane @tweakpane/plugin-essentials
the code that drives it
const pane = new Pane({ container: document.getElementById('pane'), title: 'CAN CONTROL', expanded: innerWidth > 560 });
pane.registerPlugin(Essentials);
const fps = pane.addBlade({ view: 'fpsgraph', label: 'fps', rows: 2 });

nozzle.addBinding(P, 'width', { min: .5, max: 16, step: .1, label: 'cap' }); // interval (essentials)
const pressure = nozzle.addBlade({ view: 'cubicbezier', value: [0.15, 0.85, 0.3, 1.0], expanded: innerHeight > 520, label: 'psi', picker: 'inline' });
nozzle.addBinding(P, 'drips', { min: 0, max: 1 });
// in the frame loop:
    const k = curve.y(1 - s.life / s.max);       // pressure profile from the bezier blade
    const r = s.w * Math.max(.05, k);
site ↗source ↗npm ↗MIT · v4.0.5 (plugin-essentials 0.2.1)
live demo tap to start · arrows/WASD or the pad · down = dig · up = hop ↗ full window
vanillacanvasretroaudiophysics

KAPLAY

Crate Digger: 75 seconds till closing

The community fork of Kaboom.js: a component-based JS game library

KAPLAY builds games from small component arrays (sprite, area, body, anchor), plus scenes, tweens and a built-in audio mixer. Every asset here is generated at boot. The pixel sprites are drawn from character maps onto a canvas and passed in as data URLs. The SFX and a dusty boom-bap loop are synthesized sample by sample into WAV bytes for loadSound. Dig crates, catch the records that pop out, and hunt the gold labels.

npm i kaplay
the code that drives it
const digger = sheet(12, 16, 4, (g, f) => {
  [...HEAD, ...LEGS[f]].forEach((row, y) => [...row].forEach((ch, x) => { if (PIX[ch]) { g.fillStyle = PIX[ch]; g.fillRect(x, y, 1, 1); } }));
  if (f === 3) { g.fillStyle = C.mustard; g.fillRect(10, 8, 2, 3); } // flashlight in hand
});
k.loadSprite('digger', digger, { sliceX: 4, anims: { idle: 0, run: { from: 1, to: 2, loop: true, speed: 9 }, dig: 3 } });
k.loadSound('hop', synth(.13, (t, D, ph) => { ph += (260 + 900 * t / D) / SR; return [sq(ph) * .3 * (1 - t / D), ph]; }));
// …
  const me = k.add([k.sprite('digger', { anim: 'idle' }), k.pos(240, 250), k.anchor('bot'), k.area({ scale: k.vec2(0.75, 0.95) }), k.body({ jumpForce: 330 }), k.z(5), 'player']);
  me.onCollide('loot', (r) => {
    if (k.time() - r.born < .35) return;
    k.destroy(r);
    if (r.gem) { gems++; k.play('gem', { volume: .55 }); say('GOLD LABEL: ' + k.choose(TITLES)); }
    else { wax++; k.play('pick', { volume: .45 }); say(k.choose(TITLES), C.cream); }
site ↗source ↗npm ↗MIT · v3001.0.19
live demo drag the drawbar dots · presets · tap to start sound ↗ full window
reactsvgaudiomotion

Mafs

Eight partials, one wave

React components for interactive math: plots, vectors and draggable points in SVG

Mafs turns coordinate-space math into declarative React. Plot.OfX samples functions adaptively, and useMovablePoint gives constrained drag handles with keyboard support. Eight vertically constrained points act as organ drawbars. The same eight coefficients drive a chain of rotating epicycles, the summed waveform, and a Web Audio PeriodicWave, so what you drag is exactly what you hear.

npm i mafs react react-dom
the code that drives it
for (let i = 0; i < N; i++) {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    pts.push(useMovablePoint([i + 1, PRESETS.saw[i]], { color: HUES[i], constrain: ([, y]) => [i + 1, Math.max(-1, Math.min(1, Math.round(y * 20) / 20))] }));
  }
function applyWave(amps) {
  if (!audio.ctx) return;
  const real = new Float32Array(N + 1), imag = new Float32Array(N + 1);
  amps.forEach((a, i) => { imag[i + 1] = a; });
  if (amps.every((a) => Math.abs(a) < 1e-3)) imag[1] = 1e-4;
  audio.osc.setPeriodicWave(audio.ctx.createPeriodicWave(real, imag));
// …
          <Plot.OfX y={wave} domain={[0, L]} color="#e4472b" weight={3.2} />
live demo hit a pad or keys 1–4 · drag/resize chops · drag empty space for a new region · swap kit/swing to re-render ↗ full window
vanillacanvasaudiocharts

wavesurfer.js

Chop Shop: an offline-rendered beat on a waveform deck

An audio waveform player with a plugin system: regions, minimap, timeline, spectrogram

wavesurfer.js renders and plays waveforms, and its Regions plugin makes time ranges draggable, resizable and playable. Nothing is loaded from disk. An 8-bar, 86 BPM boom-bap (kick, snare, hats, saw bass, Rhodes-ish Dm9 stabs, vinyl crackle) is rendered in an OfflineAudioContext, encoded to a 16-bit WAV blob, and handed to loadBlob with precomputed peaks. Four regions map to MPC-style pads that loop each chop, and the Minimap shows the whole side.

npm i wavesurfer.js
the code that drives it
const ws = WaveSurfer.create({
  container: '#ws', height: waveH(), waveColor: '#6b5a44', progressColor: '#ffb347', cursorColor: '#efe3cf', cursorWidth: 2,
  barWidth: 2, barGap: 1, barRadius: 2, minPxPerSec: 70, autoScroll: true, autoCenter: true, normalize: true, dragToSeek: true, backend: 'WebAudio',
  plugins: [
    regions,
    MinimapPlugin.create({ height: 26, waveColor: '#4a3b2b', progressColor: '#a8763a', overlayColor: 'rgba(255,179,71,.14)', cursorColor: '#efe3cf', barWidth: 1, barGap: 1, insertPosition: 'afterend' }),
  ],
// …
  const buf = await renderBeat(pattern, swing);
  await ws.loadBlob(toWav(buf), [buf.getChannelData(0), buf.getChannelData(1)], buf.duration);
regions.on('region-out', (r) => { if (loopOn && active && r === active.region && ws.isPlaying()) r.play(); });
site ↗source ↗npm ↗BSD-3-Clause · v8.0.3

See one you want?

Every card on this page is a real open-source library doing something most sites never try. Point at the one that fits: a launch page, an album drop, a menu board, a tool your customers actually play with. We'll art-direct it for your brand and build it into your site, fast and lightweight.