Skip to content

Hoisting

Custom properties inherit downward only, so the values a source writes on an element are visible to that element’s descendants and to nobody else — not its siblings, and (because @container style() matches an ancestor) not even the element itself. Hoisting moves just the writes: data-props-to="<selector>", or { to } on propsFor().

resize me ↘
--live-w --live-h

One binding: propsFor(box, ['size'], { to: 'figure' }). The box is what's measured; the <figure> is what's written. Drag the box's corner and its siblings answer — the ghosts copy its aspect ratio and fan out along its diagonal, the readout counts its pixels, the card tints with its proportions. Flip the toggle to drop to: the same source now writes on the box itself, where no sibling can reach it, and everything outside the box freezes at its fallback.

The box is bound to size. The <figure> around it is where the properties land, so every sibling inside the figure inherits them:

<script type="module">import 'prop-for-that/auto'</script>
<figure>
<div data-props-for="size" data-props-to="figure">resize me</div>
<div class="fan"><i></i><i></i><i></i></div>
<figcaption></figcaption>
</figure>
/* siblings of the measured box, reading its geometry */
.fan i {
aspect-ratio: var(--live-aspect); /* its proportions */
rotate: calc(atan2(var(--live-h), var(--live-w)) * var(--i)); /* its diagonal */
}
figcaption b::after {
counter-reset: w calc(var(--live-w)); /* CSS prints the number itself */
content: counter(w);
}

The imperative form is the same binding:

import { propsFor } from 'prop-for-that'
propsFor(box, ['size'], { to: 'figure' }) // selector → box.closest('figure')
propsFor(box, ['size'], { to: box.parentElement }) // or an element

Drop the to and nothing about the source changes — it still measures the box — but the properties are written on the box, where the fan and the caption can’t reach them. That’s the toggle in the demo above.

It applies to every element source, including the ones that must observe the element itself and so can’t use the container-binding trick (range, field, img… find an inner element when you bind their wrapper): size, visibility, pointer-local, truncated. The classic case is an image telling its own figure how it’s doing:

<figure>
<img data-props-for="img" data-props-to="figure" src="art.jpg">
<div class="skeleton"></div>
<figcaption></figcaption>
</figure>
@container style(--live-loaded: 0) {
.skeleton { opacity: 1; } /* shimmer while the bytes are in flight */
img { filter: blur(8px); } /* the source element reacts to its own state */
}
  • A selector resolves once, at bind time, with el.closest(). Under auto, moving an element in the DOM re-binds it, so the target is re-resolved.
  • No match degrades. If nothing matches, the properties are written on the element itself and a console.warn says why — the page keeps working, it just loses the sharing.
  • Globals ignore it. to on a global source warns and is ignored; globals already write to a shared target.
  • One binding per key per target. Two elements hoisting the same key onto one ancestor overwrite each other (and disposing either strips the property). It warns; give each its own target.

See Style queries for the @container half, and propsFor for the full API.