Close04 / 07

04 / 07

Ambient UI

Self-initiated · 2026 · Concept and development

The page at rest, before permission: the wordmark Ambient UI, the claim beneath it, and a readout running on time of day

A page with no colours of its own. It reads a block of variables and becomes them, the way a shader reads uniforms and knows nothing about the world outside itself.

That is what it is now, and not what is deployed. The live page and every screenshot here are the earlier version, a webcam colour picker; what follows is on a branch, and the sections marked as planned are not built at all.

What writes those variables is deliberately not the page’s business. A webcam frame is the first source and the one it is built to demonstrate, but a still photograph, a synthetic scene, a microphone or a light sensor can write them equally, and so can the page’s own emitted light coming back. The agnosticism is the mechanism rather than the pitch. The pitch is that the document is alive.

Interfaces are built as though they were being looked at in the same conditions everywhere. A dark mode is a preference, not a measurement — you tell the machine it is night. The room itself, which is the actual thing the screen is competing with, is never consulted. It is available: every laptop has a camera pointed at the same light the reader is sitting in.

What a source is allowed to say

A source contributes an observation — only what a sensor can physically see. Everything else in a reading is derived from it, once, in one place.

Exactly one quantity in a reading is observed: the dominant colour. Luminance follows from the background. The text pole follows from the luminance. Colour temperature follows from the dominant via the Planckian locus. Variance and direction follow from the luminance grid, and motion follows from the dominant measured against the previous sample’s.

Direction is the one that is not a row on the spec sheet. It is a vector — the intensity-weighted centroid of the grid, as an offset from centre per axis — so it is drawn as a compass rather than printed as a number. The sheet’s Distance row is a different quantity again, and not a property of the room at all: it is how far the colour on screen still is from the colour being aimed at.

The test for membership is that nothing in the observed set is computable from anything else in it. A camera cannot hand over a text pole, because the pole follows from the colour and following-from is not seeing. That rule is what keeps a source from asserting a conclusion and calling it a measurement.

The reading as a spec sheet, with every quantity the page has not measured saying so

Every one of those is on the page as a number, with a diagram showing how it was derived. Not because a visitor needs them, but because a system that changes on its own is otherwise indistinguishable from a system that is broken. The readings are the difference between a page that adapts and a page that flickers.

One channel, one source

One pass over the pixels yields seven things. Colour takes four of them — the dominant, the mean, the buckets and the histogram. Field takes the other three — the luminance grid, the colour thumbnail and the waveform. Those are the two channels, and each goes entirely to the highest-priority source that claims it. The seam is about which of the seven a source may claim, not about how many there are.

Nothing is averaged between sources, ever. A luminance grid from the camera sitting underneath a dominant colour from somewhere else would give a variance and a direction describing a frame that is not the colour on screen — a reading no room ever produced. Arbitration keeps the guarantee that the colour on the page is a colour something actually reported.

The contrast problem

The obvious way to get text colour is to invert the background. It is also wrong, and it fails in the most common case: invert a mid-grey room and you get mid-grey text.

So the text does not follow the background’s colour, it follows its luminance. There is a crossover at 0.1791 — the point where black and white give identical contrast against the same surface, computed rather than typed as a literal — and the text picks whichever side of it guarantees the ratio. Below, white. Above, black. The background can drift anywhere it likes; legibility is not part of the negotiation.

The contrast ruler, showing the fallback’s own background against its text at 18.7 to 1, beside a colour-temperature ruler with no reading on it

The pace of change

A room that is not changing should not produce an interface that is changing. Motion below 0.01 ΔE is sensor noise and damps to zero, so a still room holds still. The target carries the same deadband, so the theme holds where it is until the room moves away from it.

Above that threshold the settle falls from 3200 milliseconds toward 520 as the light keeps moving. Someone walking past a lamp gets a slow drift; opening a blind gets a faster one. A velocity cap of 0.22 ΔE per second holds the large changes back on their way in, so the pace is a function of how much the environment is doing rather than an easing curve someone picked.

Saying which half was measured

Because several sources can feed the page at once, each reading records which one supplied each channel. Every figure then asks whether its own value was measured, rather than whether the page as a whole has a camera — which is what lets it keep saying “zero is not a measurement” honestly when half the reading is estimated and the other half is not.

Five diagrams with nothing plotted in them, under four captions saying what each is waiting for: waiting for a frame, no frame to bin yet, nothing scanned yet, nothing sampled yet

Nothing leaves the device

No frame is uploaded, stored, or sent anywhere. The camera is behind an explicit control rather than a prompt on load, and the page is finished before you press it: without permission it runs on time of day and system preference, and it does not ask twice.

That is not only a privacy position. A feature that requires permission to be worth looking at is a feature most people never see.

There is also no memory across browser sessions, because a shader has none. The page is purely reactive and clueless about the outside world by design.

Written, and not yet seen on hardware

A generative ground: a full-page WebGL2 field behind the content, driven by the measured variables — direction sets its flow, variance its contrast, motion its speed, luminance its mean. It renders greyscale and composites with mix-blend-mode: luminosity, exactly as the photographs already do, so the no-accent-colour rule is obeyed rather than amended. It is correct by inspection and has only been confirmed through headless screenshots, which are an unreliable witness for anything that runs on a GPU.

Alongside it, the whole reading published as CSS custom properties rather than the four it published before. Variance in particular had existed only as a printed number, doing no visual work at all.

Planned

None of this is built.

The page as an emitter rather than only a display, in three experiments. It modulates its own background at 0.5 Hz and cross-correlates that against what the camera sees, which measures how much of the reading is the page looking at itself — echo cancellation, borrowed from audio. It flashes known colour patches to solve the 3×3 matrix the camera’s auto white balance is applying, and inverts it. And a proportional-integral controller whose actuator is the screen and whose plant is the room, to see how far the page can push the room’s measured colour temperature.

Those three are aimed at the three findings currently flagged untested. One of them ends with the sentence “this is not solvable from inside the page”. The calibration run is the attempt to prove that sentence wrong, and whichever way it lands the note gets rewritten to what was actually measured.

Last, a corpus of reference and adversarial scenes, generated procedurally rather than filmed, so they double as the test fixtures.

What it is for

It is a hook and a demonstration of it, not a page. useAmbient() returns the reading, the permission state and the provenance behind each channel — so anything consuming it can degrade knowingly.

The word shader describes how the page is structured, not how it paints. Everything that ships today is Canvas 2D and SVG; the ground that will paint like one is written and not yet shipped. Two runtime dependencies, React and React DOM. No CSS, animation or charting framework, no accent colour, and every mark on the page is currentColor at one of five strengths.

The thing I keep returning to is interfaces that read their environment instead of waiting for input. This is the second attempt; the first was a lamp.