Ronalds Vilciņš

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.

Change the context supplied to a component
View code
HTML
<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>
CSS
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; } }
JavaScript
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:

HTML
<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>
CSS
.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:

HTML
<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>
CSS
.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:

CSS
.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:

HTML
<div style="--density: compact">
  <nav class="menu" aria-label="Account">
    <a href="/profile/">Profile</a>
    <a href="/settings/">Settings</a>
  </nav>
</div>
CSS
.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 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!

← Back to all articles