Let Components React to CSS Variables With Style Queries
I like the idea of setting a theme once and letting the components inside it decide what that means. CSS style queries make that possible: descendants can change their styling based on a container’s custom property value.
The MDN container query guide
covers the model. Size queries ask about dimensions; style queries can use a value such as --theme: dark as the condition.
View code
<label>Theme <select id="theme"><option value="light">Light</option><option value="dark">Dark</option></select></label>
<label>Density <select id="density"><option value="normal">Normal</option><option value="compact">Compact</option></select></label>
<div class="context" style="--theme: light; --density: normal"><section class="card"><h2>Account settings</h2><p>The parent supplies the context; the card owns its styles.</p></section></div>
<p class="note">Unsupported queries leave the base card visible.</p>button, select { padding: .35rem .6rem; border: 1px solid var(--clg); background: var(--cw); color: var(--cb); }
input[type="range"] { width: min(100%, 18rem); vertical-align: middle; }
h2, h3 { font-size: 1rem; }
.note { color: var(--clg); }
label { display: inline-block; margin: 0 .5rem .75rem 0; }
.card { padding: 1.5rem; border: 1px solid var(--clg); background: white; color: #222; }
h2 { margin: 0; }
p { margin-bottom: 0; }
@container style(--theme: dark) { .card { background: #222; color: white; } }
@container style(--density: compact) { .card { padding: .5rem; } .card p { margin-top: .4rem; } }const context = document.querySelector('.context');
for (const name of ['theme', 'density']) {
document.querySelector(`#${name}`).addEventListener('change', event => {
context.style.setProperty(`--${name}`, event.target.value);
});
}Use style queries to respond to a theme
A wrapper can carry the theme while an inner element receives the styling:
<div class="card" style="--theme: dark">
<div class="card-inner">
<h2>A card in a dark theme</h2>
<p>The wrapper supplies the theme. The card decides its colours.</p>
</div>
</div>
.card-inner {
padding: 1rem;
background: white;
color: #111;
}
@container style(--theme: dark) {
.card-inner {
background: #111;
color: #eee;
}
}
The query checks the custom property’s value on a container ancestor of .card-inner. When that value is dark, the matching rule changes the background and text colour.
The base colours remain usable when the query is unsupported. I would keep that fallback even if the dark treatment is the intended design.
You do not need a size container
Custom-property style queries do not require container-type: inline-size. The default container type
already allows style queries.
That is different from a size query, where a container must be established for the dimensions being queried. Adding size containment just to read a theme value would introduce an unrelated layout decision.
The container query model still applies styles to descendants. The wrapper and inner element in the example have separate jobs: one supplies the condition, the other receives the result.
Let the context supply a surface value
The same card can appear in two parts of a page:
<section style="--surface: light">
<article class="surface-card">A card in the main content.</article>
</section>
<footer style="--surface: dark">
<article class="surface-card">A card in the footer.</article>
</footer>
.surface-card {
padding: 1rem;
border: 1px solid #ccc;
background: white;
color: #111;
}
@container style(--surface: dark) {
.surface-card {
border-color: #444;
background: #111;
color: white;
}
}
The surrounding element supplies the surface value. The card’s rules decide which colours suit that context, rather than making the footer assign a modifier class to each card.
Unregistered custom properties inherit , so that context can also travel through intermediate elements. An unnamed query uses the nearest eligible ancestor; an inherited value may be present there even when it was set farther up the tree.
I find that useful when several components should respond to the same context. For one isolated card, a modifier class may be easier to follow.
Keep var() for a value you can use directly
A custom property can already supply a colour without a query:
.simple-card {
background: var(--card-background, white);
}
I would keep that version when the requirement is simply to use a different background. The var() fallback
supplies white when --card-background is unset.
A style query becomes more interesting when a semantic value should change several declarations. A compact menu, for example, may need different spacing as well as smaller text.
Let a compact menu define its own spacing
The parent can request compact density without assigning every size individually:
<div style="--density: compact">
<nav class="menu" aria-label="Account">
<a href="/profile/">Profile</a>
<a href="/settings/">Settings</a>
</nav>
</div>
.menu {
display: flex;
flex-wrap: wrap;
padding: 1rem;
gap: 1rem;
}
@container style(--density: compact) {
.menu {
padding: 0.5rem;
gap: 0.35rem;
}
.menu a {
font-size: 0.9rem;
}
}
The display: flex declaration gives gap a layout where it can take effect. The query then groups the compact menu’s spacing and text adjustments in one place.
The part I like is the division of responsibility. The parent requests a density; the menu owns the actual measurements. That leaves room for a different component to interpret compact differently.
Check numeric comparisons separately
A keyword match such as style(--density: compact) does not establish support for numeric range comparisons. The style-query rules
treat value matching and ranges as different forms of condition.
I would start with named values for themes or component density. Before using a numeric comparison, check the exact syntax and browser support for that feature rather than copying a conceptual --columns >= 3 example into production.
The same caution applies to normal properties. The MDN style-query documentation
describes a broader model, but identifies custom properties as the currently supported style features. Being able to query a custom property does not mean an arbitrary declaration such as font-weight: bold is queryable.
Keep classes when the state belongs to the component
A card--featured class is still a clear way to mark one card as featured. I would not add a parent custom property solely to avoid that class.
Style queries earn their place when context belongs higher in the tree and several descendants need to respond. A theme or density setting fits that pattern; an isolated exception often does not.
For a reusable component system, I like having that choice. Use a value directly through var() when that is enough, and reach for a style query when the component needs to interpret a shared setting.
Filed under CSS on
Enjoyed this article?
If you found it useful, share it with a friend or someone who might find it helpful too. Thanks for reading!