Style Queries and Container State: Token-Driven Component Variants
Size queries let a component read how much room it has. Style queries let it read what it has been told — a value sitting in a custom property somewhere above it in the tree. This guide covers the style() half of the @container at-rule and belongs to the Mastering Container Queries & Responsive Layouts guide, extending the mechanics introduced in Container Query Syntax Basics. The payoff is architectural: a component can carry every one of its variants in one stylesheet, activated by a single inherited token, with no variant class threaded through the markup and no JavaScript deciding which class to apply.
What this guide covers:
- The evaluation model that separates
style()from size conditions - Why current implementations accept custom properties only
- Declaring, inheriting, and matching state tokens
- Replacing variant class permutations with a single token axis
Prerequisites
Before working through the code here, you should be comfortable with:
- The
@containerat-rule and how a query names or omits a container. - Custom property declaration and inheritance — that
--xset on an element flows to every descendant until redeclared. - The difference between a declared value and a computed value, since style queries match computed values.
- Basic cascade reasoning: specificity, source order, and the fact that at-rules do not contribute specificity.
- Optionally,
container-typesemantics as covered in container-type: size vs inline-size — useful context, though style queries do not require it.
Core concept: querying state instead of space
A size query asks a geometric question — is the container at least 400 pixels wide? The engine must lay the container out before it can answer, which is why size containment is required and why container-type exists at all.
A style query asks a completely different question — does the container's computed value for this property equal this value? No layout is involved. The answer depends only on style computation, which happens earlier in the rendering pipeline and does not require the container to have been sized, contained, or even rendered.
That difference has one immediate and frequently surprising consequence: every element is a style container by default. The initial value of container-type is normal, and normal still establishes a style query container. You do not opt in. A bare @container style(--tone: warning) { … } inside a rule set resolves against the parent of each matched element, because the parent is the nearest ancestor and every ancestor qualifies.
The spec language is worth internalising: a style query is evaluated against the nearest ancestor style query container, and a query container never includes the element being styled. The element's own declarations are invisible to its own query. This is not a quirk to work around; it is what makes the mechanism safe, since self-matching would create trivially circular style dependencies where a rule's outcome changes the value the rule tested. The mechanics of that ancestor rule, and the mistakes it produces, are worked through in detail in style queries with custom properties.
The custom-property-only limitation
CSS Conditional Rules Level 5 defines style() to accept any declaration, including standard properties: @container style(font-style: italic) is valid in the specification. No shipping engine implements that. As of mid-2026 Chromium, Gecko, and WebKit all restrict style() to custom properties, and a query naming a standard property simply never matches — it does not throw, warn, or fall back. It silently evaluates false.
Treat that as a design constraint rather than a temporary gap. Even when standard-property queries land, routing component state through explicit tokens is the more legible pattern: --density: compact states intent, whereas padding: 4px states an outcome and invites brittle coupling between a component's internals and the rules that test them.
Syntax and parameters
| Token | Accepted values | Default / notes |
|---|---|---|
style( … ) | A single declaration: --name: value | The only query function that reads computed style |
--name | Any custom property name | Standard property names parse but never match in shipping engines |
| value | Any token sequence; compared after computation | Matching is on computed value, whitespace-normalised |
style(--name) | Boolean form — property has a value other than its initial | Guaranteed-invalid values (unset custom properties) fail |
not style( … ) | Negation of a single query | Useful for expressing a default branch |
style(a) and style(b) | Conjunction | Each style() wraps exactly one declaration |
style(a) or style(b) | Disjunction | Parenthesise when mixing and with or |
| container name | @container card style(--x: y) | Restricts resolution to the nearest ancestor with that container-name |
container-type | Not required for style() | normal (the initial value) already qualifies as a style container |
Two syntax details cause most parse-time confusion. First, each style() holds exactly one declaration — style(--a: 1; --b: 2) is invalid; write style(--a: 1) and style(--b: 2). Second, mixing size and style conditions in one query is allowed and often useful: @container card (min-width: 400px) and style(--variant: feature) resolves the size half against the nearest ancestor named card that has a container-type, and the style half against the nearest ancestor named card regardless.
Step-by-step implementation
Step 1 — Declare the token where the decision belongs
Put the custom property on whichever element owns the decision. For a component-level variant that is the component root; for a page-region theme it is the region wrapper. Always give it a default so the query has something deterministic to read.
.card {
--variant: comfortable; /* every card has a defined value from the start */
border: 1px solid #d7dce5;
border-radius: 10px;
background: #fff;
}
.card--compact { --variant: compact; }
.card--feature { --variant: feature; }
The modifier classes here set nothing but a token. They carry no visual declarations, so they never enter a specificity contest with the rules that respond to them.
Step 2 — Write the default appearance with no query at all
Everything outside a query is what the component looks like when no token matches — in an unsupporting engine, or when the token holds an unrecognised value. Make this branch complete and correct on its own.
.card__body { padding: 1rem; color: #101828; }
.card__title { margin: 0 0 .25rem; font: 600 1rem/1.3 system-ui, sans-serif; }
.card__meta { margin: 0; color: #667085; font: 400 .8rem/1.4 ui-monospace, monospace; }
Step 3 — Add one query block per token value
Each block reads the token from the nearest ancestor. Because .card__body is a child of .card, the token declared in step 1 is exactly one hop away.
@container style(--variant: compact) {
.card__body { padding: .5rem .75rem; display: flex; align-items: baseline; gap: .6rem; }
.card__title { margin: 0; font-size: .9rem; }
}
@container style(--variant: feature) {
.card__body { padding: 1.4rem; border-left: 4px solid #7aa2ff; }
.card__title { font-size: 1.35rem; }
}
Drag your eye across the three cards below — the markup for all three is identical apart from one class that sets one custom property.
Step 4 — Combine with a size condition where both matter
Size and style answer different questions, so let each own what it is good at: geometry from the size query, intent from the token.
.panel {
--tone: neutral;
container-type: inline-size; /* required for the size half only */
border: 1px solid #d7dce5;
border-radius: 10px;
background: #fff;
padding: 1rem;
}
.panel--alert { --tone: alert; }
.panel__inner { display: flex; flex-direction: column; align-items: flex-start; gap: .6rem; }
/* Geometry */
@container (min-width: 380px) {
.panel__inner { flex-direction: row; align-items: center; gap: 1rem; }
}
/* Intent */
@container style(--tone: alert) {
.panel__badge { background: #7aa2ff; color: #10182b; }
.panel__text { font-weight: 600; }
}
Resize the frame and note that the two conditions never interfere — narrowing the panel does not change its tone, and changing its tone does not change its axis.
Annotated production example: a notification list item
This is a realistic component with three orthogonal state axes — severity, density, and read state — that would classically require a matrix of modifier classes. Here each axis is one token.
<ul class="feed" style="--density: comfortable">
<li class="feed__item" style="--severity: critical; --read: no">
<div class="feed__marker" aria-hidden="true"></div>
<div class="feed__content">
<p class="feed__title">Certificate expires in 48 hours</p>
<p class="feed__detail">edge-gateway-03 · renewal not scheduled</p>
</div>
</li>
<li class="feed__item" style="--severity: info; --read: yes">
<div class="feed__marker" aria-hidden="true"></div>
<div class="feed__content">
<p class="feed__title">Nightly backup completed</p>
<p class="feed__detail">14 volumes · 2m 11s</p>
</div>
</li>
</ul>
.feed {
--density: comfortable; /* list-level axis, inherited by every item */
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: .5rem;
}
.feed__item {
--severity: info; /* item-level axes, overriding nothing above */
--read: no;
display: grid;
grid-template-columns: auto 1fr;
gap: .75rem;
align-items: start;
padding: .9rem 1rem;
border: 1px solid #d7dce5;
border-radius: 10px;
background: #fff;
}
.feed__marker {
inline-size: 10px;
block-size: 10px;
margin-block-start: .35rem;
border-radius: 50%;
background: #98a2b3;
}
.feed__title { margin: 0; font: 600 .95rem/1.4 system-ui, sans-serif; color: #101828; }
.feed__detail { margin: .15rem 0 0; font: 400 .82rem/1.4 system-ui, sans-serif; color: #667085; }
/* Axis 1 — severity. Read from .feed__item by its own children. */
@container style(--severity: critical) {
.feed__marker { background: #b42318; }
.feed__title { color: #912018; }
}
@container style(--severity: warning) {
.feed__marker { background: #b54708; }
}
/* Axis 2 — read state. Independent of severity; the two blocks compose. */
@container style(--read: yes) {
.feed__title { font-weight: 400; }
.feed__marker { background: transparent; box-shadow: inset 0 0 0 2px #d0d5dd; }
}
/* Axis 3 — density. Declared on .feed, so .feed__item reads it from its parent. */
@container style(--density: compact) {
.feed__item { padding: .5rem .75rem; gap: .5rem; }
.feed__detail { display: none; }
}
Three points deserve attention. The density block styles .feed__item itself, which works because the token lives one level up on .feed — had it been declared on .feed__item, the item could not read it. The severity and read blocks style children of the element carrying the token, which is the ordinary arrangement. And the blocks compose freely: a critical unread item picks up declarations from two blocks with no combinatorial rule like .feed__item--critical.feed__item--unread anywhere in the sheet.
Adding a fourth severity value costs one block. Adding a fourth axis costs one block per value, not a multiplication against everything already present. That linear-instead-of-multiplicative growth is the whole argument, and it is examined against the class-based and attribute-based alternatives in container style query theming.
Performance and accessibility notes
Style queries do not force layout. Unlike size queries, which need the container's box before they can be answered, a style query is resolved during style computation. There is no containment cost, no contain side effect, and no risk of the layout thrashing that deeply nested size containers can cause. If you have been rationing container queries for performance reasons, that budget does not apply here.
The cost that does exist is invalidation breadth. Changing a token on a high element invalidates style for its whole subtree, and the engine must recompute matches for every descendant with a query that reads it. Declaring --theme on :root and querying it from a thousand nodes is fine when it changes on user action; it is wasteful when the value is animated frame by frame. Keep token mutation event-driven, not continuous.
Custom properties registered with @property compute to their specified syntax before matching, which makes comparisons predictable — --ratio: 1.0 registered as <number> matches style(--ratio: 1) where an unregistered property would compare the literal token sequences instead. The type behaviour is covered from the other direction in registered properties and type safety.
On accessibility, remember what a style query is not: a token change alters presentation only. If --state: error turns a field red, assistive technology learns nothing from it. The token must accompany an accessible signal — aria-invalid, role="alert", real text — not replace one. Colour-only distinctions between severity levels also fail WCAG 1.4.1 (Use of Color); pair every colour shift with a shape, icon, or text label, exactly as the marker-plus-title pairing does above.
Finally, tokens driving motion should still respect user preference. A --motion: expressive token must sit under the same prefers-reduced-motion discipline as any other animation trigger, per reducing motion preferences in CSS.
DevTools debugging workflow
- Confirm the engine supports the feature at all. Every current engine does, so this is a question about the version in front of you rather than the brand. Check the Styles pane for whether your
@container style(...)block is listed as an at-rule group at all: a version predating the feature omits the block entirely, which is the fastest way to tell a support problem from a code problem. - Select the queried element, not the container. In the Elements panel, the Styles pane shows matched rules grouped under their
@containerheader. If your block is absent, the query did not match for this element. - Walk one level up and read the Computed tab. Filter for the custom property name. The value shown on the parent is the value the query tested. If the property appears on the selected element but not on its parent, you have the self-declaration bug.
- Check whitespace and case in the computed value. Custom properties preserve their token stream. A trailing comment or an unexpected
Duskversusduskwill fail an equality test that looks correct in source. - Edit the token live. Change the parent's custom property value directly in the Styles pane; matching blocks appear and disappear on the child immediately. This is the fastest confirmation that resolution is wired correctly, and it also exercises invalidation so you can watch the Performance panel for recalculation cost.
- For named queries, verify the name resolves.
@container card style(--x: y)needs an ancestor withcontainer-name: card; if no ancestor matches the name, the query is false regardless of token values.
Browser compatibility
| Feature | Chrome/Edge | Firefox | Safari | Notes |
|---|---|---|---|---|
@container style(--prop: value) | 111+ | 151+ | 18+ | Custom properties only |
Boolean form style(--prop) | 111+ | 151+ | 18+ | True when the property has a non-initial value |
style() with standard properties | Not implemented | Not implemented | Not implemented | Specified, but no engine ships it |
style() combined with a size condition | 111+ | 151+ | 18+ | Size half still needs container-type |
Style container without container-type | 111+ | 151+ | 18+ | normal already qualifies |
Custom-property style queries are now implemented in all three engines: Chromium since 111, Safari since 18, and Firefox since 151. The remaining support question is therefore about older versions still in circulation rather than about a hold-out engine, and it is answered structurally — keep every style() block additive so an engine that drops it still renders a finished design. That discipline, and how to verify it, is set out in style query fallbacks and support; the general detection technique for containment is in feature detection with @supports.
Common pitfalls
| Pitfall | Cause | Resolution |
|---|---|---|
| Query never matches although the value looks right | The property is declared on the element being styled, not on an ancestor | Move the declaration to a parent, or introduce a wrapper that carries the token |
style(display: grid) does nothing | Standard properties are not implemented in any shipping engine | Route the state through a custom property instead |
| Two blocks fight and the wrong one wins | @container adds no specificity; an unrelated selector outranks both | Equalise selector specificity between blocks, or place them in an ordered layer |
| Value mismatch on a numeric token | Unregistered custom properties compare token streams, so 1.0 is not 1 | Register the property with @property and a <number> syntax |
| Theme flickers or lags on toggle | Token declared very high and mutated on every animation frame | Declare tokens close to their consumers and change them on discrete events |
FAQ
Do style queries need container-type on the ancestor?
No. Every element is a style container by default, so container-type is only required for size queries. Declaring container-type: inline-size does not disable style queries, and omitting it does not prevent them.
Can I query a standard property like color or display?
Not in any shipping engine as of mid-2026. The specification allows querying non-custom properties, but every engine that ships style() restricts it to custom properties, so style(color: red) never matches.
Why does my style query fail when the element sets the property itself? A style query is evaluated against the nearest ancestor container, and an element is never its own container. Move the declaration to a parent element and the query will match.
How do style queries interact with specificity?
The @container at-rule adds no specificity at all. Rules inside it carry exactly the specificity of their own selectors, so a style query cannot outrank a more specific selector declared elsewhere.
Specification references
- CSS Conditional Rules Module Level 5 — defines
style(), the boolean form, and the style query container concept. W3C Spec - CSS Containment Module Level 3 — defines
container-type: normaland the query container hierarchy. W3C Spec - CSS Properties and Values API Level 1 —
@propertyregistration and computed-value behaviour for custom properties. W3C Spec
Related
- Style queries with custom properties — the exact matching and inheritance rules.
- Container style query theming — one token replacing a matrix of theme classes.
- Style query fallbacks and support — keeping the enhancement additive for older engine versions.
- Cascade layers for reset and tokens — where token declarations belong in an ordered cascade.
- Fluid spacing tokens driving transition durations — the same token discipline applied to motion.
Related articles
More pages in the same section.