Skip to main content

Container Queries vs. Style Queries: When to Use 'style()' Logic

 The mental model for responsive design has shifted. For years, we relied on the Viewport (@media). Recently, we gained the ability to query the Parent's dimensions (@container (min-width: ...)).

However, Design System Architects often hit a wall when solving for context rather than space. You have a card component. It looks perfect on a white background. You place that same card into a dark-themed sidebar or a branded hero section, and the contrast fails.

The immediate reaction is often to use a dimensional container query or revert to prop-drilling (e.g., <Card variant="inverted" />). Both are architectural smells. Dimensional queries shouldn't handle theming, and prop-drilling defeats the purpose of composition.

This is the exact use case for Container Style Queries.

The Architecture Gap: Dimensions vs. State

The confusion stems from conflating Layout with Logic.

  1. Dimensional Queries (sizeinline-size): These solve the "Not enough space" problem. They trigger layout shifts (Grid to Flex, hiding columns).
  2. Style Queries (style()): These solve the "Wrong context" problem. They allow a child component to react to the computed value of a CSS Custom Property on its parent.

When developers try to theme components based on min-width, they are hacking state management into a layout engine. When they use rigid BEM modifiers (.card--dark), they introduce high coupling; the parent must explicitly know the child's internal class naming convention, or the child must know the parent's class.

True decoupling requires the parent to broadcast a signal (a CSS variable) and the child to subscribe to it (a Style Query).

The Fix: Context-Aware Theming

We will build a StatsCard component. By default, it uses a standard light theme. When placed inside a PromotionalBanner, it automatically inverts its colors without React props or utility class overrides.

1. The Setup (CSS)

We utilize CSS Custom Properties as state signals. Note that currently, browsers primarily support querying Custom Properties (--var), not standard properties (like background-color).

/* GLOBAL TOKENS */
:root {
  --color-surface-default: #ffffff;
  --color-text-default: #171717;
  --color-surface-inverted: #2a2a2a;
  --color-text-inverted: #f5f5f5;
  --color-brand-primary: #3b82f6;
}

/* 
  CONTAINER SETUP 
  We define a named container. This acts as the 'State Provider'.
*/
.theme-container {
  /* 
     This name allows children to target this specific ancestor 
     layer, preventing conflict with layout containers.
  */
  container-name: theme-context;
  
  /* 
     For style queries, we don't strictly need 'container-type: inline-size', 
     but defining the custom property is essential.
  */
}

/* THEME: DARK MODE CONTEXT */
.context-dark {
  /* This variable is the 'Signal' we will query later */
  --context-mode: dark;
  background-color: var(--color-surface-inverted);
  color: var(--color-text-inverted);
  padding: 2rem;
  border-radius: 8px;
}

/* THEME: BRAND CONTEXT */
.context-brand {
  --context-mode: brand;
  background-color: var(--color-brand-primary);
  color: white;
  padding: 2rem;
  border-radius: 8px;
}

2. The Component (CSS)

The component logic lives entirely within CSS. It queries the theme-context container.

.stats-card {
  /* Default State (Light Mode) */
  background-color: var(--color-surface-default);
  color: var(--color-text-default);
  padding: 1.5rem;
  border: 1px solid #e5e5e5;
  border-radius: 6px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  transition: all 0.2s ease;
}

.stats-card h3 {
  margin-top: 0;
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #666;
}

.stats-card .value {
  font-size: 2rem;
  font-weight: 700;
}

/* 
  LOGIC: If we are in a Dark Context
  Query: Is --context-mode equal to 'dark'?
*/
@container theme-context style(--context-mode: dark) {
  .stats-card {
    background-color: rgba(255, 255, 255, 0.1); /* Glassmorphism */
    border-color: rgba(255, 255, 255, 0.2);
    color: var(--color-text-inverted);
    box-shadow: none;
  }
  
  .stats-card h3 {
    color: rgba(255, 255, 255, 0.7);
  }
}

/* 
  LOGIC: If we are in a Brand Context
*/
@container theme-context style(--context-mode: brand) {
  .stats-card {
    background-color: white;
    color: var(--color-brand-primary);
    border: none;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  .stats-card h3 {
    color: var(--color-brand-primary);
    opacity: 0.8;
  }
}

3. The Implementation (HTML/JSX)

The markup remains semantic and clean. The hierarchy dictates the styling, not the props.

export default function Dashboard() {
  return (
    <div style={{ display: 'flex', gap: '2rem', flexDirection: 'column' }}>
      
      {/* Scenario 1: Default Context */}
      <section>
        <h2>Default Context</h2>
        <div className="stats-card">
          <h3>Revenue</h3>
          <div className="value">$42,000</div>
        </div>
      </section>

      {/* Scenario 2: Dark Sidebar Context */}
      <section className="theme-container context-dark">
        <h2>Dark Context</h2>
        <div className="stats-card">
          <h3>Revenue</h3>
          <div className="value">$42,000</div>
        </div>
      </section>

      {/* Scenario 3: Brand Hero Context */}
      <section className="theme-container context-brand">
        <h2>Brand Context</h2>
        <div className="stats-card">
          <h3>Revenue</h3>
          <div className="value">$42,000</div>
        </div>
      </section>
      
    </div>
  );
}

Why This Works

The magic lies in the separation of concerns via the style() function.

  1. Named Containers (theme-context): By naming the container, we bypass immediate parents that might not have the style data we need. The query looks up the DOM tree until it finds the nearest ancestor with container-name: theme-context. This solves the issue where intermediate <div> wrappers break CSS selectors like .context-dark > .stats-card.
  2. Custom Property Logic: We are not querying background-color. We are querying --context-mode. This is an abstraction layer. It decouples the visual implementation of the parent (what color it is) from the logical state (what "mode" it is in).
  3. Encapsulation: The .stats-card owns its appearance for every context. The parent container doesn't need to know CSS overrides for its children.

Conclusion

Stop using dimensional queries (min-width) to guess the theme of your components. Stop drilling theme props into every leaf node of your React tree.

Use Container Style Queries to create reactive, context-aware components. Define semantic signals in your layout containers (--context-mode), and let your components subscribe to those signals using @container style(...). This results in a design system that is robust, decoupled, and significantly easier to maintain.