
Contextual Component Theming with CSS Container Style Queries
Modular component architecture has historically struggled with contextual theming. While CSS custom properties solved the problem of passing raw design tokens down the DOM tree, varying an entire component's visual language based on its placement—such as rendering an inverted badge inside a dark sidebar or applying bordered variants inside promotional banners—has traditionally required tightly coupled descendant class selectors like .dark-theme .card or fragile JavaScript prop drilling. These legacy techniques break component encapsulation, inflate selector specificity, and create brittle dependencies where components must know the internal naming conventions of every parent surface that might host them.
CSS Container Style Queries, defined in the CSS Containment Module Level 3 specification, introduce a robust mechanism for contextual component styling. Unlike size-based container queries that evaluate inline width or block height, style queries evaluate the computed values of CSS custom properties on an ancestor container via @container style(--property: value). Every HTML element capable of holding custom properties can act as a style container without requiring an explicit container-type: size, enabling parent wrappers to establish thematic environments purely by setting variables such as --theme: dark or --surface: elevated. Child components query their nearest ancestor container to adopt contextual typography, borders, shadows, and interactive states autonomously, achieving true design encapsulation.
Example
<!-- Parent section declares contextual theme intent -->
<section class="marketing-card" style="--surface-theme: high-contrast;">
<h2 class="card-title">Analytics Snapshot</h2>
<button class="action-button">Export Report</button>
</section>
/* Container declaration */
.marketing-card {
container-name: surface;
padding: 1.5rem;
border-radius: 0.5rem;
}
/* Child component responds to container custom property */
@container surface style(--surface-theme: high-contrast) {
.action-button {
background-color: #000000;
color: #ffffff;
border: 2px solid #ffffff;
box-shadow: 0 4px 0 #000000;
}
}
Implementation
/* ==========================================================================
Contextual Design System using CSS Container Style Queries
========================================================================== */
/* 1. Base Variables & Tokens */
:root {
--color-canvas-light: #f8fafc;
--color-canvas-dark: #0f172a;
--color-text-main: #1e293b;
--color-text-subtle: #64748b;
--color-accent-primary: #3b82f6;
--color-accent-contrast: #60a5fa;
--color-surface-border: #e2e8f0;
}
/* 2. Theme Containers */
.theme-surface {
container-name: theme-surface;
padding: 2rem;
border-radius: 12px;
background-color: var(--surface-bg, #ffffff);
border: 1px solid var(--surface-border, var(--color-surface-border));
transition: background-color 0.25s ease, color 0.25s ease;
}
/* Thematic context assignments */
.theme-surface--light {
--theme: light;
--surface-bg: #ffffff;
--surface-border: #e2e8f0;
}
.theme-surface--dark {
--theme: dark;
--surface-bg: #0f172a;
--surface-border: #334155;
}
.theme-surface--featured {
--theme: featured;
--surface-bg: #4f46e5;
--surface-border: #4338ca;
}
/* 3. Reusable UI Components Querying Container Styles */
/* Primary Button Component */
.ui-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
border: 1px solid transparent;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
/* Default / fallback styling */
background-color: var(--color-accent-primary);
color: #ffffff;
}
/* Adaptation within Dark Surface */
@container theme-surface style(--theme: dark) {
.ui-button {
background-color: #3b82f6;
color: #ffffff;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.35);
}
.ui-button:hover {
background-color: #60a5fa;
}
}
/* Adaptation within Featured / Brand Surface */
@container theme-surface style(--theme: featured) {
.ui-button {
background-color: #ffffff;
color: #4f46e5;
font-weight: 700;
}
.ui-button:hover {
background-color: #f1f5f9;
}
}
/* Status Badge Component */
.ui-badge {
display: inline-block;
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 9999px;
background-color: #f1f5f9;
color: #475569;
}
@container theme-surface style(--theme: dark) {
.ui-badge {
background-color: #1e293b;
color: #94a3b8;
border: 1px solid #334155;
}
}
@container theme-surface style(--theme: featured) {
.ui-badge {
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
backdrop-filter: blur(4px);
}
}
/* Typography Headings */
.ui-heading {
margin: 0 0 0.5rem;
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-main);
}
@container theme-surface style(--theme: dark) {
.ui-heading {
color: #f8fafc;
}
}
@container theme-surface style(--theme: featured) {
.ui-heading {
color: #ffffff;
}
}
/* Body Text Subtitle */
.ui-subtitle {
margin: 0 0 1.5rem;
font-size: 0.95rem;
color: var(--color-text-subtle);
}
@container theme-surface style(--theme: dark) {
.ui-subtitle {
color: #94a3b8;
}
}
@container theme-surface style(--theme: featured) {
.ui-subtitle {
color: #c7d2fe;
}
}
/* 4. Progressive Enhancement Check for Browser Support */
@supports not (container-name: test) {
/* Legacy fallback using attribute selectors if needed */
.theme-surface--dark .ui-button {
background-color: #3b82f6;
color: #ffffff;
}
}
Main Point: CSS Container Style Queries decouple component variants from rigid descendant selectors by letting child elements query custom properties directly on their parent container.
Credits
Photo by Scott Webb on Unsplash