Video color
CSS can’t read a video’s pixels, so it can’t tint a player’s chrome, glow, or caption to match what’s on screen. The video-color plugin samples a playing <video> every few frames and writes its dominant color and a vibrant accent, each as a single #rrggbb color you drop straight into var().
It’s the moving-picture sibling of img-color: the same 16×16 downsample and bucketing, but driven by the video’s own frames instead of a one-shot load. (It keeps to these two colors rather than the full image palette — a six-swatch extraction isn’t worth it 4×/second for an ambient glow.)
The frame is read 16×16 on requestVideoFrameCallback (throttled
to ~4 Hz), so the caption — var(--live-video), the dominant
colour, with text flipped from its own lightness — and the glow and accent chip
— var(--live-video-accent), the vibrant blob — both track the
picture and stop the moment the video pauses or scrolls offscreen. The video is
a canvas-captured stream, so there's no file and no cross-origin taint.
The player’s caption is painted with the video’s dominant color and the glow with its accent, both recomputed ~4×/second as the picture changes — all composed in CSS.
The props
Section titled “The props”Written on the bound element on the live cadence. The pixels are sRGB, so each is a single hex color — no separate channel props, since CSS pulls channels out of a color for you (relative color syntax, color-mix()).
| Property | What it is |
|---|---|
--live-video | the dominant color (the most common one) |
--live-video-accent | the most vibrant color (reuses the dominant when a frame is essentially grayscale) |
.player { box-shadow: 0 0 4rem var(--live-video-accent); }.caption { background: var(--live-video); }
/* tint, fade, or mix straight from the color */.scrim { background: oklch(from var(--live-video) l c h / 50%); }
/* contrasting text: flip black/white from the background's own lightness */.caption { color: oklch(from var(--live-video) clamp(0, (0.6 - l) * 100, 1) 0 0); }How it stays cheap
Section titled “How it stays cheap”A video is a moving picture, so the cost is in when you sample, not just how:
- Sampling rides
requestVideoFrameCallback— it fires once per presented frame and stops firing when the video is paused, offscreen,display:none, or in a background tab. An idle video costs nothing, with noIntersectionObserverneeded. - On top of that, the pixel read is throttled to ~4 Hz — plenty to track scene cuts for ambient theming, a fraction of the work of reading every frame.
- Each read draws the current frame to a 16×16 canvas and buckets those 256 pixels once — the dominant and the accent both fall out of that single pass, the same path as
img-color. A paused/poster frame is seeded on attach, and whererequestVideoFrameCallbackis missing it falls back to thetimeupdateevent (~4 Hz while playing).
The wiring
Section titled “The wiring”<script type="module">import 'prop-for-that/auto'</script><!-- the video-color plugin loads itself the first time the attribute is seen -->
<!-- bind the figure so the caption (a sibling of the video) inherits the color --><figure data-props-for="video-color"> <video src="/clip.mp4" muted autoplay loop playsinline></video> <figcaption>…</figcaption></figure>import { propsFor, register } from 'prop-for-that'import { videoColor } from 'prop-for-that/plugins'
register(videoColor)propsFor(document.querySelector('figure'), ['video-color'])