Skip to content

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 through calc().
  • @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.

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 range wrapper can react.
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; }
}

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().

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).