A First Look at CSS Style Queries and Container-Based Custom Property Conditions
Style queries let a container's custom property value drive component variants, replacing BEM modifier classes with a single source of truth on the container.
You are still toggling classes on a parent element to switch a component between variants. Every toggle is a small JavaScript event that could be a declarative CSS rule. The fix is CSS style queries: a @container rule that tests the computed value of a custom property on an ancestor, so the component’s own styles react to a token without any script. This page shows you the exact syntax, the three most common variant patterns, and the honest interop picture you need before shipping to production.
Style queries let a component change its appearance based on a custom property value set on its container, enabling variant logic without class-switching. The test is written with the style() function inside @container: @container style(–variant: compact) { … }. The queried ancestor must be an explicit container. It needs a container-type declaration (usually inline-size for size queries, or normal if you only care about style) and optionally a container-name to disambiguate multiple containers. The custom property is read at its computed value after the cascade has run, so inheritance, specificity, and source order all apply before the query sees anything.
What this replaces is the BEM modifier class chain. Instead of .card–compact .card__title { font-size: 0.9rem }, you set –variant: compact on the container and write @container style(–variant: compact) { .card__title { font-size: 0.9rem } }. The component’s internal selectors stay flat, and the variant state lives in a single custom property that any ancestor can set. Here is the complete card component:
.card-container {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
gap: 1rem;
padding: 1rem;
border: 1px solid #ccc;
}
@container card style(--variant: compact) {
.card {
grid-template-columns: 1fr;
padding: 0.5rem;
}
.card__title {
font-size: 0.9rem;
}
}
@container card style(--variant: wide) {
.card {
grid-template-columns: 2fr 1fr;
}
}
Set –variant: compact or –variant: wide on .card-container in your HTML or via a class, and the card re-layouts itself. The same pattern applies to any component whose visual variation is a matter of tokens rather than geometry. The second sample is a button that changes size and shape from the same property, replacing a component with multiple size classes like .btn–sm, .btn–md, .btn–lg. The button’s container is its parent, and the variant is set once.
.btn-wrapper {
container-type: inline-size;
container-name: btn;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 1rem;
}
@container btn style(--size: small) {
.btn {
padding: 0.25rem 0.5rem;
border-radius: 2px;
font-size: 0.8rem;
}
}
@container btn style(--size: large) {
.btn {
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1.2rem;
}
}
The third sample is a theme section that responds to a –theme token, replacing a data-attribute selector theme system. Where you might have written [data-theme=’dark’] .section { background: #111 }, you now set –theme: dark on the section’s container and query it. The advantage is that the theme token is a custom property, which can be inherited, overridden, and animated, things a data attribute cannot do on its own.
.theme-wrapper {
container-type: normal;
container-name: theme;
}
.section {
background: #fff;
color: #111;
}
@container theme style(--theme: dark) {
.section {
background: #111;
color: #eee;
}
}
@container theme style(--theme: sepia) {
.section {
background: #f4ecd8;
color: #5b4636;
}
}
Each of these samples replaces a different failure mode. The first replaces the BEM modifier class chain, which scatters variant styles across multiple class definitions and requires JavaScript to swap them. The second replaces a component with multiple size classes, which multiplies CSS and HTML for what is really one token. The third replaces a data-attribute selector theme system, which cannot inherit, cannot be computed from other custom properties, and cannot be animated without extra JavaScript.
Building a Fallback
Before you commit to style queries in production, you need the fallback. The technique is a class-based selector before the @container block that achieves the same visual result for browsers without style query support. Write the default styles first, then the fallback class that applies the variant, then the @container block that overrides it when supported. For the card example:
.card { … default … }
.card--compact { … compact styles … }
.card--wide { … wide styles … }
@container card style(--variant: compact) {
.card { … compact styles … }
}
Alternatively, use an @supports guard. The spec allows testing for style query support via @supports not (container-type: style), but that syntax is awkward because container-type: style is not a real value in most engines. The more reliable guard is @container style(–supported: true) { … }, which only matches when style queries work, with the fallback outside. The fallback class approach is simpler to maintain because it does not require duplicating the query logic.
Browser Support Reality
Now the honest part: CSS style queries are Baseline limited as of mid-2025. They have shipped in Blink (Chrome, Edge, Opera) and WebKit (Safari), but Gecko (Firefox) has interop gaps. The Web Platform Status dashboard tracks this, and the Interop project publishes per-engine WPT subtest failure counts. Check that dashboard before committing to production use, because the number changes with each Firefox release. As of the 2025 Interop cycle, style queries are not yet in the “widely available” Baseline bucket.
Common Pitfalls
The common mistakes are worth naming so you do not trip on them. First, you cannot query shorthand properties: @container style(background: red) is invalid, because only longhand or custom properties are queryable. Second, style queries require an explicit container-type or container-name on the queried ancestor, they do not work on a plain div. Third, the custom property must be set on the container itself, not on a descendant, because the query reads the container’s computed value, not the element being styled.
One more caveat: style queries combine with size container queries in a single @container rule. The syntax is @container (min-width: 400px) and style(–theme: dark) { … }. This is powerful, but it means the size query and the style query share the same container. If you need different containers for size and style, you must nest them or use container-name to target different ancestors.
The FAQ below answers the four questions people actually ask when they first meet style queries.
Now the sections that answer each secondary keyword in turn. The first is the style() container query syntax itself, which you have seen in every sample. The second is component variant logic CSS, which is the entire point of this page: moving variant state from classes to custom properties. The third is style queries vs size container queries, the distinction between a value condition and a measurement condition. The fourth is custom property condition styling, which is what makes the whole mechanism work.
Style() Container Query Syntax
The style() function is the core of the syntax. It takes a property and a value, and the query matches when the container’s computed value for that property equals the given value. For custom properties, the syntax is @container style(–variant: compact). You can combine conditions: @container style(–a: 1) and style(–b: 2), or use not and or. The parentheses around each style() call are required. The container must be named or typed via container-name or container-type before the query can see it.
Component Variant Logic CSS
Component variant logic in CSS has historically meant class toggling: .card–compact, .card–wide, .btn–sm. Style queries replace that with a single custom property that any ancestor can set. The component’s internal styles react to that token, and the variant state is part of the cascade, not part of the JavaScript. This is declarative: you describe the condition, and the browser applies the styles. It is also composable, because a custom property can be computed from other custom properties via calc() or var() fallbacks.
Style Queries vs Size Container Queries
A size container query responds to the container’s dimensions, using min-width, max-width, or other measurement conditions. A style query responds to the computed value of a custom property, which is a value condition, not a measurement. The distinguishing feature is the type of condition: a value vs a measurement. Size queries change with viewport or container geometry; style queries change when a token changes. They are orthogonal, and you can combine them in a single @container rule.
Custom Property Condition Styling
The custom property condition is what makes style queries a constraint-solving system. You set –theme: dark on a container, and every descendant that queries that container adapts. The custom property cascades and inherits like any other property, so you can set it on a high-level wrapper and let it propagate. You can also compute it: –theme: var(–user-preference, light). This is closer to a design-system token than a class, because it carries meaning through the cascade.
Here is a table that summarizes the three patterns and what each one replaces:
| Pattern | Replaces | Condition Type |
|---|---|---|
| Card variant | BEM modifier class chain | style(--variant: compact) |
| Button size | Multiple size classes (.btn--sm) | style(--size: small) |
| Theme section | Data-attribute selector | style(--theme: dark) |
The failure cases are the ones you will hit in real code. If your style query does not respond, check that the container has container-type declared. If you are querying a shorthand, switch to a longhand or a custom property. If the custom property is not updating, check the inheritance chain: the property must be set on the container, not on a descendant that does not inherit it. If you are combining with a size query, make sure the same container satisfies both conditions.
The honest caveat is the interop gap. Style queries are not in Firefox as of mid-2025, and the WPT subtest failures are tracked on the Interop dashboard. The spec is at the W3C CSS Containment Module Level 3, and MDN documents the syntax. Before you build a design system on style queries, verify the current support in your target browsers, and keep the fallback class in place until Firefox catches up.
Style Query Syntax FAQ
What is the exact style() container query syntax for a custom property?
Write <code>@container style(--my-prop: value) { … }</code>. The <code>style()</code> function wraps the custom property name and the value you are testing. You can chain conditions with <code>and</code>, <code>or</code>, and <code>not</code>, and combine with size queries using parentheses.
Can I query a shorthand property like background or margin?
No. Style queries only accept longhand properties or custom properties. Querying <code>background</code> is invalid. Use the longhand, such as <code>background-color</code>, or move the value into a custom property and query that.
Do I need container-type on the ancestor for style queries to work?
Yes. The queried ancestor must be an explicit container. Declare <code>container-type: inline-size</code> for size queries, or <code>container-type: normal</code> if you only need style. Without it, the query has no container to read.
What is the fallback for browsers without style query support?
Duplicate the variant styles in a class-based selector before the <code>@container</code> block. The class applies the same visual result, and the <code>@container</code> block overrides it where supported. This keeps the page usable in Firefox and older engines.