Dynamic Selectors
conceptReactive pseudo-classes match from state: a sibling by index, a checkbox after it changes, an element right after a state changes, or a field after it loses focus.
On this page · 6 sections+−
:nth-var()
:nth-var(--name) matches the element whose one-based index among its element siblings equals the integer value of --name, read on the parent of the siblings. It has the same specificity as :nth-child().
.tabs { --active: state(1);}.tab:on(click) { --active: set(sibling-index());}.panel { display: none;}.panel:nth-var(--active) { display: block;}.tab:nth-var(--active) { border-color: currentColor;}When --name is a state(), the cell is read directly and only the old and new active elements are touched. It also works with a plain custom property, such as .carousel { --active: 3; }.
For plain custom properties, a sibling group is re-checked on DOM and attribute changes, including el.style.setProperty(). Changes that come only from media queries, container queries, animations or stylesheet edits need ReactiveCSS.refresh(). For calc() values, register the property as <integer> with @property.
:just-checked and :just-unchecked
:just-checked matches a checkbox or radio after an observed unchecked → checked change, until its next observed change. It never matches on page load. :just-unchecked is the reverse, including a radio that loses its state when another one in its group is selected.
.task { --done: state(false);}/* The span next to the checkbox reads the state, so it lives on .task. */.task input:on(change) { --done: set(event(checked));}.task input:just-checked + span { animation: pop 300ms;}.task input:just-unchecked + span { animation: settle 300ms;}.task span { opacity: when(--done, 0.55, 1);}Why it stays on
There are no timers, so a cancelled or interrupted animation can’t leave a stuck state. On each change the markers swap and the animation starts again. The cost: non-animation declarations such as scale: 1.2 stay applied until the next change, so write those with native :checked.
Setting input.checked = true fires no event. Dispatch new Event('change', { bubbles: true }) afterwards, or call ReactiveCSS.refresh().
:changed()
:changed(--x) matches briefly after an observed change of the nearest state --x: from the frame after the change until the animations and transitions it started in the element’s subtree have finished, or one frame if none started. “It started” means the animations named, and the transitions of properties set, in the :changed() rules: an unrelated animation that restarts in the same frame does not hold it. It never matches on load or after a no-op write.
.cart:changed(--items) .count { animation: bump 240ms;}:blur
:blur matches an element from a real focus loss until it is focused again, never on load. Like :just-checked, it marks a moment, so an animation on it plays once per blur:
input:blur:invalid { animation: shake 250ms;}Use .form:has(:blur) for a container. Details and the difference from :not(:focus): Focus & Blur.
Focus is not a selector here
:focus, :focus-visible and :focus-within stay native. Reactive CSS has no functional :focus(): to move focus when a state becomes true, use the declaration focus: when(condition).