The main rules you need to know about observed markers and runtime limits are collected on this page.
Core Rules
Section titled “Core Rules”Marker Must Be at the End
Section titled “Marker Must Be at the End”? and ?! are written at the very end of the selector part, without a space. If a pseudo is present, the marker comes after the pseudo. In a descendant chain, the marker is written on the last element where CSS is applied, not on intermediate selectors. If a selector is observed with ? or ?!, that selector is accepted as the observed root within that source.
/* correct */.card?.card?!.btn:hover?.btn:focus?!.accordion-item.is-open .accordion-body?
/* wrong */.btn?:hover.btn?!:focus.card ?.card ?!.accordion-item.is-open? .accordion-bodyObserved Root Is Inherited by Nested Selectors
Section titled “Observed Root Is Inherited by Nested Selectors”If the outer selector is observed, it is not necessary to write ? or ?! again on nested selectors.
.card? { .title { color: red; } &:hover { color: blue; }}
@media (min-width: 920px) { .card { display: flex; }}In this source .card .title, .card:hover, and .card inside @media become dynamic observed. If .card?! were used, the same inheritance would work as initial observed.
Root matching is done from the beginning of the selector:
.card .title,.card:hover,.card > .titlebecome observed.layout .card,.x + .card,:has(.card)do not become observed
Does Not Work Inside @keyframes
Section titled “Does Not Work Inside @keyframes”The observed marker has no meaning inside @keyframes and @-webkit-keyframes.
/* wrong usage */@keyframes fade { to? { opacity: 1; }}Do Not Use It on Global Selectors
Section titled “Do Not Use It on Global Selectors”Do not use observed markers on :root, html, body, or global reset * selectors. These selectors already exist in normal documents, so observed checks do not provide useful savings.
/* wrong / unnecessary */:root? { --brand: #2060ff; }html? { min-height: 100%; }body? { margin: 0; }*? { box-sizing: border-box; }
/* correct */:root { --brand: #2060ff; }html { min-height: 100%; }body { margin: 0; }* { box-sizing: border-box; }*? is syntactically valid, but it is very broad. Use it only when you truly need "any element inside this subtree exists."
Class / Id Changes Are Not a State System
Section titled “Class / Id Changes Are Not a State System”Observed tracks selector presence. The runtime can use class and id mutations for reevaluation because those changes can alter whether a selector matches in the DOM.
.card.is-active? { outline: 2px solid var(--c-accent);}This rule can become active when .is-active appears and deactivate when the class is removed. But if .card is always in the DOM and .is-active is only UI state, static CSS is usually clearer:
.card.is-active { outline: 2px solid var(--c-accent);}Do not use observed as a general-purpose state system; reserve it for cases where the element may genuinely be absent from the DOM.
Works Inside @media and @supports
Section titled “Works Inside @media and @supports”Observed selectors can be used inside @media and @supports. The at-rule condition is checked first, then the DOM selector check runs.
@media (min-width: 920px) { .card? { display: flex; }}Comma-Separated Selector Parts Are Evaluated Separately
Section titled “Comma-Separated Selector Parts Are Evaluated Separately”Observed and normal selectors can be mixed with commas in the same rule. The compiler classifies each selector part separately.
.card?, .panel { color: red; }.d .card, .card, .card .title { padding: 8px; }Result:
.cardbecomes observed.panelstays static.d .cardstays static.cardand.card .titlebecome observed
Descendant Chain Is Checked for the Entire Selector
Section titled “Descendant Chain Is Checked for the Entire Selector”.a .b .c? { color: red;}In this case not just .c but the entire .a .b .c chain must match in the DOM.
Do Not Use for Interaction State
Section titled “Do Not Use for Interaction State”Observed looks at whether a selector is or is not in the DOM. It does not track user interaction states like :hover, :focus, :active in an event-based way.
Clear rule:
- observed = DOM presence
- static selector = interaction state
/* don't rely on these */.tooltip-wrap:hover .tooltip?.btn:focus?.btn:active?!
/* correct approach */.tooltip? { opacity: 0; }.tooltip-wrap:hover .tooltip { opacity: 1; }Observed makes sense for elements like tooltips, modals, toasts that are sometimes in the DOM and sometimes not. But hover/focus/active visibility or style changes should be written as native CSS selectors.
.card? and .card:hover? Are Not the Same Thing
Section titled “.card? and .card:hover? Are Not the Same Thing”.card? { box-shadow: 0 1px 4px var(--c-shadow);}
.card:hover? { box-shadow: 0 4px 16px var(--c-shadow-md);}This writing is valid. But the meaning of the second line is:
.card?→ if.cardis in the DOM, the base rule becomes active.card:hover?→ if.cardis in the DOM, the hover rule is also loaded
This does not mean "observe when hovered." Hover changes are not watched separately by observed; hover state still works with native CSS.
So the clearer writing in most cases is:
.card? { box-shadow: 0 1px 4px var(--c-shadow);}
.card:hover { box-shadow: 0 4px 16px var(--c-shadow-md);}Short recommendation:
.card?or.card?!→ makes sense when the element is sometimes not in the DOM.card:hover?or.card:hover?!→ unnecessary and confusing in most cases- write hover/focus/active state styles as static selectors
::before? and ::after? Work Through the Host Element
Section titled “::before? and ::after? Work Through the Host Element”.tooltip? { opacity: 0;}
.tooltip::after? { content: ''; border-top-color: var(--c-text);}This writing is valid. But the meaning is:
.tooltip?→ if.tooltipis in the DOM, the base rule becomes active.tooltip::after?→ if.tooltipis in the DOM, the::afterrule is also loaded
This does not mean there is a separate observed existence check for the pseudo-element. Since ::before and ::after are not real DOM nodes, observed bases on the host element.
So the clearer writing in most cases is:
.tooltip? { opacity: 0;}
.tooltip::after { content: ''; border-top-color: var(--c-text);}Short recommendation:
.tooltip?or.tooltip?!if the host element is sometimes not in the DOM- leave the pseudo-element rule static in most cases
* Is Valid But Wide
Section titled “* Is Valid But Wide”.player *?This writing is valid. But since * is a very wide target, it can be unnecessarily expensive on the observed side. If possible, prefer a narrower selector over *?:
.player .title?.player .controls?.player button?
:not(), :where(), and Similar
Section titled “:not(), :where(), and Similar”Observed can be used with these kinds of selector pseudo-functions. But ? or ?! is always written on the outer tail, not on the sub-selector inside the parentheses.
/* correct */.shell:not(.is-collapsed)?.menu:where(.is-ready, .is-open)?!.panel:is(.wide, .stacked)?
/* wrong */.shell:not(.is-collapsed?).menu:where(.is-ready?!, .is-open).panel:is(.wide?, .stacked)Short Note for + and ~
Section titled “Short Note for + and ~”.lead + .item?.lead ~ .note?!These selectors are valid. Matching is always verified over the full selector. But re-evaluation depends on the token-index optimization. So for sibling relationships that change frequently, some structural changes like another node being inserted in between may not always be re-checked immediately.
Short recommendation:
- if the sibling relationship is very dynamic, prefer a static selector
- observed makes more sense for "sometimes present, sometimes not" scenarios
Limits
Section titled “Limits”| Limit | Default | Short meaning |
|---|---|---|
maxObservedRules | 5,000 | Total observed rule count in the source |
maxObservedActiveRules | 1,000 | Number of observed rules that can be active at the same time |
maxObservedRuleKeysPerToken | 300 | Maximum rule key count that can be tracked for a DOM token |
maxMutationBatchTokens | 1,500 | Number of tokens that can be processed in a single mutation batch |
maxObservedReevalPerTick | 500 | Number of observed rules that can be re-evaluated in a single tick |
What Generally Happens When a Limit Is Exceeded?
Section titled “What Generally Happens When a Limit Is Exceeded?”- if the default policy is
fail-closed, compilation stops with an error - if
fail-soft, compilation continues and aresource-limitwarning may be produced - the behavior on exceeding each limit is not the same; some do filtering, some do trimming, some apply a silent boundary on the runtime side