Vital
One lumen — light and cells through the body's channels. One theme, one tempo, six rhythms.
A lumen is two things: a unit of light, and the interior channel of a vessel. That pun is the system's whole motion theme — everything that moves in Lumen is light and cells moving through the body's channels. Not a metaphor applied to the components; the rule the components are derived from.
Every waiting surface is a view of the same channel:
| Surface | The channel, … |
|---|---|
Beam / RouteProgress | …straight on — light running the vessel |
Spinner | …looped — the same light bent into a ring; the head is the light-front |
Skeleton sweep | …diffused — the light passing behind tissue |
Loader | …inhabited — cells riding the pulse on the vessel line |
VitalField | …radiant — the light scattering loose at high presence |
The three organs
The wider motion language has exactly three primitives, each owning its anatomy. The lumen (this page) — waiting and process. The eye — the five-dot identity: it breathes and blinks on its own terms in Brand, and the loader never borrows it. The body — the kinematic figure in the movement zoo: gaits and celebration belong to a body with legs, not to cells. If a motion idea doesn't fit its organ, it doesn't ship.
One tempo, two meters
Every duration in the language is a stated multiple of one pulse — --vital-tempo (1.25s, a
resting heart). The cardiac rhythms and the wait-light run on the pulse; the respiratory rhythms run
in triple meter over it. That shared heartbeat — not a shared color, not a shared easing — is what
makes six rhythms and four light-scales read as one organism.
| ×0.75 | ×1 | ×1.5 | ×3 | ×4.5 | |
|---|---|---|---|---|---|
| Light | orbit | beam | sweep | ||
| Cardiac | heartbeat · pulse · perfuse | ||||
| Smooth | peristalsis | breath | sleep |
Retuning the organism is one token. The truth script enforces the timeline: TEMPO in TypeScript must
equal --vital-tempo, the light's durations must derive from it, and the registry may hold only beat
multiples from these two meters — a third meter needs a ruling, not a commit.
The six rhythms
All interior, all involuntary — motions a channel can actually make. Rendered live from the package registry; change the register and the same rhythms dial up and down.
Cardiac meter — on the pulse (×1)
The pump and what it drives. Reach here when the user is watching.
Heartbeat ×1
The pump — lub-dub, then a diastolic rest. The default.
Pulse ×1
The pressure wave arriving downstream — one clean swell.
Perfuse ×1
Cells riding the wave down the channel — the row reads left to right.
Triple meter — over the pulse (×1.5 · ×3 · ×4.5)
The channel’s slow muscle and the breath above it. Calm and ambient-adjacent work.
Peristalsis ×1.5
The channel itself squeezes the flow along.
Breath ×3
The respiratory swell over the pulse — triple meter, calm work.
Sleep ×4.5
The breath of sleep — the slowest register the channel has.
Which one
heartbeat for a wait the user is watching. perfuse when the row should read left-to-right.
peristalsis for something moving through a pipeline. breath for calm work, sleep for the
ambient edge. There is deliberately nothing here that walks, dances, or blinks — cells have no feet
and no eyelids. (The old gait and play sketches live on in the
loader zoo, the sketchbook for the body-figure's future vocabulary.)
Presence — how much the system shows up
A loader that looks identical prefetching in the background and blocking the screen is lying about one
of those situations. Presence is the dial: one rhythm, four registers, scaling amplitude, bloom, tempo,
and the field's density off one variable (--vital-amp, multiplied into every keyframe).
dormant
still — the resting pose
resting
amp 0.55 · glow 0.25 · ×0.92
attentive
amp 1 · glow 0.55 · ×1
urgent
amp 1.25 · glow 1 · ×1.12
| Register | Use it for |
|---|---|
dormant | stillness — the resting pose. Motion is earned by a signal, never ambient; an instrument at rest holds its pose. (The identity may breathe — that's the eye's privilege, not the lumen's.) |
resting | background work the user didn't ask for |
attentive | the default: a wait the user is watching |
urgent | long waits and hero moments — it leans in, and the light scatters |
Presence cascades through [data-presence], so a whole region dials at once. Paired with the timing
contract, a wait that goes long changes its bearing, not just its words:
const { pending, long } = usePending(active);
<Loader rhythm="heartbeat" presence={long ? 'urgent' : 'attentive'} />Variance — why it reads alive
A loop that repeats exactly is a machine. A living rhythm wanders: real hearts have heart-rate variability, and its absence is a clinical warning sign. So the system's pulse wanders — every registered element drifts ±8% along a slow, deterministic curve, each starting at its own phase, and no two loaders on a screen ever tick together.
Toggle variance off in the zoo above and the grid snaps into lockstep. Nothing else changes. That difference is the whole argument.
Why JS owns tempo
CSS dials amplitude (--vital-amp is multiplied into every keyframe — which is why presence is a
dial, not four hand-tuned copies of each rhythm). It cannot dial tempo: changing
animation-duration restarts a loop, while playbackRate glides. So the driver owns tempo, and
everything still works with JS off — it just runs even.
Components that own their presence (Loader) drive themselves. For everything else — a Skeleton, a
Beam, a Spinner rendered on the server — mount VitalRoot once and it adopts every [data-vital]
element beneath it:
import { VitalRoot } from 'lumen/react';
<VitalRoot>
<App />
</VitalRoot>The field — the channel, radiant
At urgent the light doesn't stay inside the cells: it scatters. Systole throws light outward,
diastole draws it home — and it is genuinely one clock: the field phase-locks to the first cell's real
Animation.currentTime, wander included, and reads its accents from the same registry beat map the
keyframes encode. Canvas and CSS land their energy on the same instant.
The palette obeys the transillumination law the tokens already state — light through blood deepens,
it doesn't bleach: the scatter is the warm ramp only (--glow → --primary → --primary-text),
thinning as it deepens. No cool flecks (data's color), no camera bokeh — this is the same light as the
sweep, the beam, and the spinner, one scale up. It overflows the mark's box deliberately (hero use),
stays out of the accessibility tree, and paints one still frame under reduced motion.
The arrival — the wait's ending
The contract is meticulous about a wait's beginning (180/420ms) and its escalation (long → urgent).
The arrival owns the third beat: when the wait resolves, the cells glide to rest on the vessel
line, the field draws its light home, and only then does the answer rise in on the exhale. One
gesture, one duration — --duration-settle, ×0.32 of the tempo, so even the ending sits on the timeline.
Oura synced
312 new samples — resting heart rate 54 bpm.
Arrival choreographs it: placeholder while pending, the settle handshake when the wait resolves, then
the enter. Placeholders with nothing to land (skeletons, spinners) skip straight to the rise — the
common fast path costs nothing over the old instant swap. And content that was never waited for never
animates: a page of Arrivals doesn't ripple on load, because motion is earned by a signal.
const { pending, long } = usePending(isLoading);
<Arrival pending={pending} placeholder={<Loader size={40} presence={long ? 'urgent' : 'attentive'} />}>
<Reading data={data} />
</Arrival>A custom placeholder can join the handshake with useSettle:
const { settling, onSettled } = useSettle();
React.useEffect(() => {
if (settling) playMyExit().then(onSettled);
}, [settling, onSettled]);Under reduced motion there is nothing to land and nothing rises — the answer simply appears, and a settler that can't play its landing counts as landed.
Reduced motion
Every animation lives inside a prefers-reduced-motion: no-preference guard — enforced by the truth
script, which fails the build if one escapes. Under reduced motion
the cells settle onto the vessel line, the field paints one still frame, and the driver never starts.
Presence still reads: urgent's bloom is visible even at rest.
API
import {
Loader, Arrival, useSettle, VitalRoot, VitalField,
RHYTHMS, PRESENCE, TEMPO, useVital,
} from 'lumen/react';
<Loader rhythm="perfuse" presence="resting" />
<Loader size={44} presence="urgent" /> {/* the scatter comes along */}
<Loader rhythm="heartbeat" variance={0} /> {/* opt out: a true metronome */}
// drive your own element with the language
const vital = useVital({ presence: 'attentive' });
<svg ref={vital} data-rhythm="breath">…</svg>RHYTHMS is the registry — id, name, meter, beat multiple, per-cell lags, and the beat map
(accents) that both the keyframes and the field read. The truth script checks every entry has real
keyframes behind it and that every duration sits on the tempo timeline, so the theme can't drift apart
one commit at a time.
→ Loading — the ladder and the timing contract · Animation — the motion families · The loader zoo — the sketchbook