jelly-progress-bar
v0.1.0
Published
GPU-accelerated "liquid fill" progress bar for React. A fullscreen-quad Three.js ShaderMaterial does all per-pixel work; the CPU only integrates two springs.
Maintainers
Readme
jelly-progress-bar
GPU-accelerated "liquid fill" progress bar for React. A single fullscreen-quad WebGL shader does all per-pixel work (wavy jelly edge, big-wave light field, waterline glow, squeeze pulse); the CPU only integrates two tiny spring scalars. Colors and every visual parameter are props — no CSS framework required.
Extracted from the training pipeline of splatscene, a Tauri 3D Gaussian Splatting app. The shipped defaults are the exact visuals that app settled on.
Why?
| | Typical progress bar | JellyProgressBar | | --- | --- | --- | | Fill | static color / CSS transition | wavy jelly waterline that bounces on every value jump | | Body | flat color block | big-wave light field, crests anchored in space | | Edge | none | diffusing waterline glow (asymmetric, both sides) | | Motion | none | squeeze pulse band near the moving edge | | CPU cost | — | 2 scalar springs per frame (2–4 float ops) | | Rendering | DOM/CSS | one WebGL quad, every pixel computed in parallel on the GPU |
This is the component to reach for when you have a GPU already in the page (three.js, gsplat viewers, dashboards) and want a progress bar that reads as alive — a liquid filling a vessel — for free on the CPU.
Install
npm i jelly-progress-barPeer dependencies (not bundled): react ≥18 <20 and three ≥0.150.
Consumers already bundling three pay nothing extra.
Quick start
import { JellyProgressBar } from "jelly-progress-bar";
export function TrainingPipeline() {
return (
<JellyProgressBar value={42} height={64}>
<span>Training… 42%</span>
</JellyProgressBar>
);
}value is a target level 0–100. The waterline eases toward it every frame
and, when the target jumps, bounces like jelly before settling. children render
as an overlay on top of the bar (stage label + live percent + metrics).
Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | number | — (required) | Target level 0–100. Read live every frame; the eased display level follows with a first-order lag. |
| children | ReactNode | — | Overlay content rendered above the WebGL canvas (e.g. stage label + percent). |
| height | number | 64 | Bar height in px. |
| className | string | — | Extra class names forwarded to the root element (jpb-root is always applied). |
| tuning | Partial<Tuning> | — | Visual tuning overrides, merged over DEFAULT_TUNING. See Tuning. |
| colors | Partial<Colors> | — | Color overrides, merged over DEFAULT_COLORS. See Colors. |
| cornerRadius | number | tuning.cornerRadius | Convenience override for the corner radius in px (shell borderRadius + shader rounded-box mask stay in sync). |
All props update live — no renderer rebuild, no remount. Change tuning,
colors, cornerRadius, height, or value mid-flight and the next frame
reflects it.
Tuning
15 visual parameters. Pass a partial object; unspecified keys keep their defaults.
| Key | Default | Range (sane) | What it does |
| --- | --- | --- | --- |
| edgeGlowThick | 0.3 | 0.05–1 | Half-width of the waterline glow band, in px-scale multiples. |
| edgeGlowBright | 1.15 | 0–2 | Multiplier on the waterline glow color. |
| edgeGlowLeft | 9 | 1–20 | How far the glow diffuses behind the edge (into the filled side). |
| edgeGlowRight | 1 | 1–20 | How far the glow diffuses ahead of the edge (into the unfilled side). |
| edgeGlowSpill | 6 | 0–20 | Extra px of filled color that "spills" past the wavy edge before fading. |
| jellyBounce | 0.3 | 0–1 | Underdamping of the jelly spring — 0 is sluggish, 1 is springy/bouncy. |
| waveAmp | 0.75 | 0–2 | Amplitude of the big-wave light/dark field inside the fill. |
| waveScale | 0.5 | 0.2–2 | Wavelength scale of the big waves (larger = longer, smoother waves). |
| waveSpeed | 1 | 0–3 | How fast the wave phase drifts. |
| morphAmp | 0.25 | 0–1 | How much the wave crests warp/morph over time. |
| squeezeAmt | 0.045 | 0–0.2 | Strength of the squeeze pulse: while the edge moves, a band near it brightens. |
| glowExp | 2.5 | 0.5–5 | Fall-off exponent dimming the waves toward the left wall (1 = linear). |
| baseBright | 0.85 | 0–2 | Brightness of the base fill gradient. |
| cornerRadius | 0 | 0– | Corner radius of the box (px). 0 = square. Also sets the shell borderRadius. |
| levelDamp | 0.7 | 0.1–1 | Damping of the first-order level lag — how quickly the target eases into position. |
<JellyProgressBar
value={60}
tuning={{ jellyBounce: 0.6, waveAmp: 1.1, cornerRadius: 8 }}
/>Colors
Four colors. Each is a hex string (#RGB or #RRGGBB). Invalid values throw
immediately (Invalid hex color "…").
| Key | Default | Description |
| --- | --- | --- |
| baseTop | #7F3C14 | Fill gradient color at the top of the bar. |
| baseBottom | #4B1407 | Fill gradient color at the bottom of the bar. |
| waterlineGlow | #FFE8A2 | Color of the glow hugging the moving edge. |
| track | #12090c | Background color of the empty bar (also the shell background). |
<JellyProgressBar
value={80}
colors={{ baseTop: "#1E88E5", baseBottom: "#0D47A1", waterlineGlow: "#80DEEA", track: "#0A1929" }}
/>The fill gradient is mix(baseBottom, baseTop, 1 − y/uH) — top color at the top,
bottom color at the bottom, smoothly interpolated by the fragment shader.
Exports
import {
JellyProgressBar, // component
DEFAULT_TUNING, // { …15 defaults }
mergeTuning, // Partial<Tuning> → Tuning (sanitizes NaN/Infinity)
type Tuning,
DEFAULT_COLORS, // { baseTop, baseBottom, waterlineGlow, track }
mergeColors, // Partial<Colors> → Colors (validates hex)
normalizeHex, hexToRgb, // color utils (hexToRgb → normalized 0–1 [r,g,b])
type Colors,
cx, type ClassInput, // tiny class-join helper
clamp, easeLevel,
stepJelly, stepSqueeze, // the CPU spring physics, pure & testable
type SpringState,
} from "jelly-progress-bar";Accessibility
- Renders
role="progressbar"witharia-valuemin="0",aria-valuemax="100". aria-valuenowis updated every tick from the eased level (viasetAttribute, outside React's render cycle, so re-renders never clobber it).- Honors
prefers-reduced-motion: the wave time freezes, the edge snaps to the target instantly, and the squeeze pulse is disabled. - The WebGL canvas is created inside
useEffectin atry/catch. In environments without WebGL (jsdom, headless, SSR hydration) the component falls back to a styled track + overlay, and the canvas is simply absent.
Browser support
WebGL1+ (everything modern). The component clamps devicePixelRatio to 2 to
bound fill-rate cost. ESM-only; pair it with any bundler. SSR-safe (all DOM
work happens in effects).
Development
npm install
npm run demo:dev # tune-slider playground (Vite, imports ../src live)
npm run typecheck # tsc across src/test/demo
npm test # vitest (pure physics/color tests + jsdom render test)
npm run build # tsc → dist (ESM, .d.ts, source maps)
npm pack --dry-run # inspect the tarball (dist + README + LICENSE only)Architecture
For the deep dive — the 5-point visual model, the exact spring equations, the full uniform table with per-parameter semantics, the live-update flow, reduced motion / fallback behavior, performance notes, and a porting guide for vanilla / Canvas2D / WebGPU — see docs/ARCHITECTURE.md.
License
MIT © 2026 chrislin95
