Ambient light on the GPU
A WebGL2 canvas that sums soft Gaussian light sources into a field of colour. Each source reads a semantic colour token at runtime, so the whole thing re-themes with the page and with any scoped custom-property override. It is the background of this site: what you are looking at behind this page is this component.
It is scenery, not interface. Nothing it does is a signal, so its motion can never be mistaken for feedback. That is the licence for a full-viewport surface to move at all.
Default
Eight sources on the core accent tokens, drifting on 14 to 22 second cycles. The field fills its positioned ancestor and ignores pointer events, so placement, clipping and any mask belong to the box you put it in.
Parameters
Seven numbers describe the look. They pass through as a partial, merged over the shipped defaults, so changing one thing takes one property. Two stages rather than seven, deliberately: every field on this page is a live GL context, and a documentation page that stands up eight of them is teaching the wrong lesson about what this component costs.
Composition
A composition is a table of token names with positions, sizes and drift periods. Weights are emission: positive sources give off light, and a negative one absorbs it, which is how you cut a shadow that occludes whatever shines behind it.
One token is never in the table: --color-action-primary-bg. The action colour is reserved for calls to action and focus rings, and a full-viewport decorative field is exactly the use that would stop it meaning “click here”.
Cursor wake
The field can react to the pointer: a swirl around it, and a glow that brightens whatever colour it is passing over rather than adding one of its own. It ships dormant at react: 0, built rather than absent. While a wake is alive the render loop steps from 30fps to 60fps, then drops back, so the cost is paid only while it shows. Move across this one.
Fallback and status
The component never decides what to paint instead of itself: that is a design decision, not a rendering one. It reports how it resolved and leaves the fallback to you. No WebGL2, a blocked or lost GPU context, a renderer that stalls before its first frame, and enabled={false} all arrive at the same status, so one fallback covers every way the field can fail.
Pending is the state worth handling. While the context is coming up, painting the fallback and swapping it a frame later reads as two backgrounds loading in sequence. So paint neither, and let a watchdog bound the wait rather than leaving the page with no background at all.
Reduced motion
The system’s motion tokens collapse under prefers-reduced-motion, but that guard is CSS and cannot see a requestAnimationFrame loop. A GPU-driven surface has to check the query itself, and this one does: it draws a single static frame and never starts the loop. The rule against component-level motion queries still holds for everything that consumes the duration tokens: an animation loop sits outside that contract by construction.