Container Queries Meet Color Tokens: Components That Theme Themselves
Container queries unlock context-aware components. Combined with color-mix() and CSS custom properties, every card can pick its own legible theme based on the surface it lands on.
Container queries shipped in every modern browser by mid-2023 and changed how we build responsive components. Pair them with color-mix() and you get something even more powerful: components that retune their own colors based on the container they live in.
The setup: container types and named contexts
.surface-default { container-type: inline-size; container-name: surface; --tone: light; }
.surface-raised { container-type: inline-size; container-name: surface; --tone: light-alt; }
.surface-brand { container-type: inline-size; container-name: surface; --tone: brand; }
Each surface declares its tone. Children read it through inherited custom properties.
Components that adapt with container-style queries
.card { background: var(--surface); color: var(--text); }
@container surface style(--tone: brand) {
.card {
background: color-mix(in oklch, var(--brand), white 88%);
color: color-mix(in oklch, var(--brand), black 25%);
border-color: color-mix(in oklch, var(--brand), transparent 70%);
}
}
Drop the same .card on a brand surface and it retones itself - no extra props, no JavaScript.
Recipe: legible labels on any background
.tag {
--bg: var(--surface);
background: var(--bg);
color: color-mix(in oklch, var(--bg), black 70%);
}
@container surface style(--tone: brand) { .tag { --bg: color-mix(in oklch, var(--brand), white 80%); } }
@container surface style(--tone: dark) { .tag { --bg: color-mix(in oklch, var(--brand), black 60%); color: white; } }
One tag component, three contexts, always legible. The container drives the style; the component does not need to know its parent.
Recipe: container-size-aware density
@container surface (max-width: 320px) {
.card { padding: 0.5rem; gap: 0.25rem; --label-color: color-mix(in oklch, var(--text), transparent 25%); }
}
On narrow surfaces, dim the secondary labels so the title can dominate. The card adapts whether it is sitting in a sidebar (240px) or a hero (1200px).
Why this matters for design systems
- Fewer variants. Buttons, tags and cards no longer need
variant="onBrand". Drop them on a brand surface; they figure it out. - Themeable third-party widgets. Embeds inherit container vars, so they look native without per-product props.
- Less prop drilling. Token decisions live near the surface, not threaded through React props.
One gotcha
Container style queries currently only check custom properties (not arbitrary declarations). Always expose surface intent through a custom prop like --tone or --surface-role, never through HTML attributes.
Generate a brand-aware token set that already includes the --tone variants in our Design System tool.