Style queries
CSS consumes the properties prop-for-that writes in two ways:
var()interpolates on a value. Best for continuous numbers (--live-pointer-x-ratio,--live-value-pct) fed throughcalc().@container style()applies a conditional rule when a property equals a value. Best for discrete, boolean, or enum state (--live-online,--live-visible,--const-has-entered,--live-value: 100,--live-paused,--live-battery-low, capability tiers).
Style queries are what make the discrete signals shine: a value flips whole rule blocks on and off, which var() can’t do.
The container rule
Section titled “The container rule”A style query matches against an element’s nearest ancestor container, and for style queries every element is a container by default (no container-type needed). So:
- Global sources write to
:root, an ancestor of everything, so they’re queryable anywhere. - Element sources write to the bound element, so its descendants query it. (An element can’t style-query its own property, only an ancestor’s.) That’s exactly why a gauge inside the container-bound
rangewrapper can react.
Hoisting an element’s values to an ancestor
Section titled “Hoisting an element’s values to an ancestor”That ancestor rule is the whole reason data-props-to (imperatively, propsFor(el, keys, { to })) exists. An <img> that writes --live-loaded on itself can’t query it, and neither can the <figcaption> next to it. Hoist the writes one level up and both can:
<figure> <img data-props-for="img" data-props-to="figure" src="art.jpg"> <div class="skeleton"></div> <figcaption>Sunset over the pier</figcaption></figure>.skeleton { opacity: 0; transition: opacity 0.3s; }
/* the <figure> is the query container for everything inside it — including the <img> that produced the value */@container style(--live-loaded: 0) { .skeleton { opacity: 1; } /* sibling: shimmer while it loads */ img { filter: blur(8px); } /* the source element reacts to its own state */}@container style(--live-broken: 1) { figcaption::after { content: ' (image unavailable)'; }}The element is still the one being observed — only the writes move. See propsFor’s to option for the resolution rules (selector via closest(), no-match fallback, one binding per key per target).
Example 1: a global boolean (offline)
Section titled “Example 1: a global boolean (offline)”import { register, propsFor } from 'prop-for-that'import { online } from 'prop-for-that/plugins'
register(online)propsFor(['online']) // writes --live-online (1/0) to :root.offline-banner { display: none; }
/* :root is an ancestor container, so this works anywhere on the page */@container style(--live-online: 0) { .offline-banner { display: block; } body { filter: grayscale(1); }}Example 2: an element milestone (slider at max)
Section titled “Example 2: an element milestone (slider at max)”<script type="module">import 'prop-for-that/auto'</script>
<!-- auto binds it from the attribute; descendants of #meter inherit --live-value --><div id="meter" data-props-for="range">…</div>/* .gauge is inside #meter and inherits --live-value, so its descendants can match the exact integer, which var()/calc() can't express */@container style(--live-value: 100) { .gauge__num { color: var(--accent); } .gauge__flag::after { content: 'max'; }}Example 3: a capability tier (proposed capability plugin)
Section titled “Example 3: a capability tier (proposed capability plugin)”/* ship lighter media on lite connections */@container style(--live-delivery-mode: 1) { .hero-video { display: none; } .hero-poster { display: block; }}Equality today, ranges later
Section titled “Equality today, ranges later”Style queries match on equality (style(--live-net-type: 4)), not comparisons. Range queries (style(--live-battery-level < 0.2)) aren’t stable across browsers yet.
That suits this library: emit discrete booleans and tiers from JS, and you get threshold styling that works today. The battery plugin’s --live-battery-low (1/0), or a capability tier, lets you write:
@container style(--live-battery-low: 1) { .autoplay { animation-play-state: paused; }}No waiting for range queries. Pair continuous values with var(), discrete values with @container style().
Browser support
Section titled “Browser support”Style queries for custom properties ship in Chromium 111+, Safari 18+, and Firefox 128+. Where unsupported, @container style() blocks are ignored, so treat them as progressive enhancement and keep a sensible default outside the query (as the examples above do).