MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Modern CSS: Subgrid, Container Queries, and Layers

All modern CSS features now widely supported: subgrid, container queries, cascade layers.

CSSSubgridContainer QueriesFrontend

By MinhVo

Introduction

CSS has undergone a quiet revolution. Features that were once considered "future CSS" are now supported in all major browsers. Container queries let components respond to their own size instead of the viewport. Subgrid lets child elements align to a parent grid's tracks. Cascade layers give developers explicit control over specificity. These features fundamentally change how you write CSS, moving from viewport-centric responsive design to truly component-driven styling.

For years, developers relied on media queries to create responsive layouts. But media queries respond to the viewport, not the component. A card component that looks great in a full-width layout breaks when placed in a sidebar because the media query still sees the full viewport width. Container queries solve this by letting the component respond to its own container's size. This is the most significant CSS feature since Flexbox.

Subgrid addresses a long-standing limitation of CSS Grid. Previously, nested grids could not align with their parent grid's tracks. If you had a grid of cards where each card had a title, description, and footer, the titles across cards could not align with each other unless they were all the same height. Subgrid lets child grids inherit their parent's track definitions, enabling true alignment across nested structures.

Cascade layers (@layer) solve the specificity wars. Instead of fighting with !important or increasingly specific selectors, you can declare the order of your style layers explicitly. Third-party styles, framework styles, component styles, and utility styles each live in their own layer, and their precedence is determined by the layer order, not by specificity.

This guide covers each feature with practical examples, browser support information, performance implications, and migration strategies from older CSS patterns. By the end, you will have a complete understanding of how to use these features in production applications.

Modern CSS features

Container Queries: Component-Driven Responsive Design

How Container Queries Work

Container queries let you style elements based on the size of their containing element rather than the viewport. The browser establishes a "containment context" on a parent element, and child elements can query that context's dimensions. This is fundamentally different from media queries, which always measure the viewport.

To declare a containment context, apply container-type to the parent element. The inline-size value creates a containment context along the inline axis (typically horizontal in left-to-right layouts). This is the most common and safest option because it avoids breaking height-dependent layouts.

/* Declare containment context */
.card-container {
  container-type: inline-size;
  container-name: card;
}
 
/* Query the container */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1rem;
  }
 
  .card-image {
    aspect-ratio: 1;
  }
}
 
@container card (min-width: 700px) {
  .card {
    grid-template-columns: 300px 1fr;
  }
 
  .card-title {
    font-size: 1.5rem;
  }
}

Container queries also support style queries (querying computed style values), though this feature has more limited browser support:

@container card style(theme: dark) {
  .card {
    background: #1a1a1a;
    color: #ffffff;
  }
}

Container Query Units

Container queries introduce new length units that are relative to the container's dimensions. The cqi unit represents 1% of the container's inline size, while cqb represents 1% of the container's block size. These units enable fluid typography and spacing that scales with the container rather than the viewport.

.card-container {
  container-type: inline-size;
}
 
.card-title {
  /* Font size scales with container width */
  font-size: clamp(1rem, 3cqi, 2rem);
  line-height: 1.2;
}
 
.card-body {
  /* Padding scales with container */
  padding: clamp(0.75rem, 2cqi, 1.5rem);
}

This is particularly useful for widget-style components that appear in varying container sizes across a dashboard or CMS layout.

Browser Support and Performance

Container queries are supported in Chrome 105+, Firefox 110+, Safari 16+, and Edge 105+. The containment model has minimal performance overhead because the browser already implements layout containment internally. Container queries do not trigger additional layout recalculations beyond what contain: inline-size already establishes.

The key performance consideration is that container queries with container-type: inline-size establish layout containment on the parent element. This means the parent's intrinsic sizing is determined by its children in isolation, which can occasionally produce unexpected results with percentage-based heights or intrinsic sizing.

Container queries diagram

CSS Subgrid: Cross-Element Alignment

The Problem Subgrid Solves

Before subgrid, CSS Grid could not propagate track definitions to nested grids. Consider a product grid where each card has an image, title, description, and price. If the cards are direct children of the grid, they align perfectly. But if each card is wrapped in a container (for styling or JavaScript purposes), the nested elements lose alignment with elements in other cards.

Subgrid solves this by allowing a nested grid to inherit its parent's track definitions. The child grid does not define its own columns or rows; instead, it uses subgrid as the value for grid-template-columns or grid-template-rows, inheriting the parent's tracks.

/* Parent grid */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  grid-template-rows: auto auto auto auto;
  gap: 2rem;
}
 
/* Child grid using subgrid */
.product-card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 4; /* Card spans 4 rows: image, title, description, footer */
}
 
/* All titles align across cards, regardless of image height */
.product-card-title {
  /* No explicit sizing needed - inherits from parent grid */
}
 
.product-card-description {
  /* Aligns with descriptions in other cards */
}

Subgrid in Both Axes

Subgrid can be used in one or both axes. Using grid-template-columns: subgrid and grid-template-rows: subgrid simultaneously creates a nested grid that fully inherits both column and row tracks from its parent. This is useful for complex dashboard layouts where widgets need to align both horizontally and vertically.

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(3, auto);
  gap: 1rem;
}
 
.widget {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
  grid-column: span 2;
  grid-row: span 2;
}

Gap Handling in Subgrid

When using subgrid, the gap property on the child grid is ignored for the subgrid tracks. The parent grid's gap values are applied instead. This ensures consistent spacing across all nested elements. If you need additional spacing within a subgrid item, use padding on the child elements rather than gap on the subgrid container.

Browser Support

Subgrid is supported in Chrome 117+, Firefox 71+ (the earliest implementer), and Safari 16+. Edge follows Chrome's implementation. The feature has no performance overhead beyond normal grid layout.

Cascade Layers: Solving the Specificity Wars

How Cascade Layers Work

The CSS cascade determines which styles win when multiple rules target the same element. Traditionally, specificity (inline styles, IDs, classes, elements) and source order determined the winner. This created problems when integrating third-party stylesheets, framework CSS, or utility classes that conflicted with custom component styles.

Cascade layers (@layer) add a new step to the cascade that takes precedence over specificity. Styles in later-declared layers override styles in earlier-declared layers, regardless of selector specificity. This means a .button class in a components layer will override a #special-button ID selector in a base layer.

/* Declare layer order - later layers have higher precedence */
@layer reset, base, components, utilities;
 
@layer reset {
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}
 
@layer base {
  body {
    font-family: system-ui, sans-serif;
    line-height: 1.6;
    color: #333;
  }
 
  a {
    color: #2563eb;
    text-decoration: none;
  }
}
 
@layer components {
  .button {
    padding: 0.5rem 1rem;
    border-radius: 0.375rem;
    font-weight: 600;
  }
 
  .card {
    background: white;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    padding: 1.5rem;
  }
}
 
@layer utilities {
  .hidden {
    display: none;
  }
 
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
  }
}

Unlayered Styles Have Highest Precedence

Styles that are not placed in any layer have the highest precedence in the cascade, overriding all layered styles. This is intentional: it allows developers to write quick overrides without fighting layer ordering. However, it also means you must be deliberate about which styles go in layers and which remain unlayered.

@layer base, components;
 
@layer base {
  .button { color: blue; } /* Layered */
}
 
/* Unlayered - wins over both layers */
.button { color: red; }

Importing Styles into Layers

External stylesheets can be imported directly into layers using @import with the layer() function. This is the recommended approach for integrating third-party CSS without specificity conflicts.

@import url("normalize.css") layer(reset);
@import url("vendor-components.css") layer(vendor);

This ensures that vendor styles never override your custom component styles, regardless of selector specificity.

Nested Layers

Layers can be nested to create sub-categories within a layer. This is useful for large design systems where you want to organize components by category while maintaining a single parent layer.

@layer components {
  @layer buttons {
    .button { padding: 0.5rem 1rem; }
  }
 
  @layer cards {
    .card { background: white; border-radius: 0.5rem; }
  }
 
  @layer forms {
    .input { border: 1px solid #d1d5db; padding: 0.5rem; }
  }
}
 
/* Reference nested layers */
@layer components.buttons {
  .button-primary { background: #2563eb; }
}

Architecture and Design Patterns

Component-Driven Responsive Design

Replace media queries with container queries for truly portable components. A card component that adapts to its container width works identically in a full-width layout, a sidebar, a modal, and a split view without any container-specific CSS.

/* Old approach: media queries (viewport-dependent) */
@media (min-width: 768px) {
  .sidebar .card { flex-direction: row; }
}
@media (min-width: 1024px) {
  .main .card { flex-direction: row; }
}
 
/* New approach: container queries (component-dependent) */
.card-wrapper {
  container-type: inline-size;
}
 
@container (min-width: 400px) {
  .card { flex-direction: row; }
}

Responsive Layout with Nested Container Queries

Container queries can be nested. A sidebar component queries its own container, and elements within the sidebar query the sidebar's container. This creates a hierarchy of responsive breakpoints that are entirely component-driven.

/* Layout container */
.layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  container-type: inline-size;
}
 
@container (min-width: 768px) {
  .layout {
    grid-template-columns: 250px 1fr;
  }
}
 
@container (min-width: 1200px) {
  .layout {
    grid-template-columns: 250px 1fr 300px;
  }
}
 
/* Sidebar with its own container queries */
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}
 
@container sidebar (max-width: 250px) {
  .sidebar-nav {
    flex-direction: column;
  }
 
  .sidebar-nav-label {
    display: none;
  }
}

Design System with Layers

Organize your design system using cascade layers to create a clear precedence hierarchy. Tokens and custom properties live in the lowest layer, followed by resets, base styles, component styles, and utility classes. This structure ensures that utility classes always win over component styles, and component styles always win over base styles.

@layer tokens, reset, base, patterns, components, utilities, overrides;
 
@layer tokens {
  :root {
    --color-primary: #2563eb;
    --color-primary-dark: #1d4ed8;
    --color-surface: #ffffff;
    --color-text: #111827;
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-4: 1rem;
    --space-8: 2rem;
    --radius-sm: 0.25rem;
    --radius-md: 0.5rem;
    --radius-lg: 1rem;
  }
}
 
@layer base {
  body {
    color: var(--color-text);
    background: var(--color-surface);
  }
}
 
@layer components {
  .button {
    background: var(--color-primary);
    color: white;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
  }
 
  .button:hover {
    background: var(--color-primary-dark);
  }
}
 
@layer utilities {
  .text-center { text-align: center; }
  .mt-4 { margin-top: var(--space-4); }
}

Step-by-Step Implementation

Building a Responsive Card Grid with Container Queries

This example demonstrates a product card grid where each card adapts its layout based on its container width, not the viewport. The cards work correctly in a full-width layout, a sidebar, and a split-pane view.

<div class="product-grid">
  <div class="card-wrapper">
    <article class="product-card">
      <img src="product.jpg" alt="Product" class="product-image">
      <h3 class="product-title">Product Name</h3>
      <p class="product-description">A detailed description of the product...</p>
      <div class="product-footer">
        <span class="price">$29.99</span>
        <button class="button">Add to Cart</button>
      </div>
    </article>
  </div>
  <!-- More cards -->
</div>
/* Grid layout */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 2rem;
}
 
/* Card containment */
.card-wrapper {
  container-type: inline-size;
}
 
/* Card base (mobile-first) */
.product-card {
  display: grid;
  grid-template-rows: auto auto auto auto;
  gap: 0.5rem;
  background: white;
  border-radius: 0.75rem;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
 
.product-image {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
}
 
/* Responsive card with container query */
@container (min-width: 400px) {
  .product-card {
    grid-template-columns: 200px 1fr;
    grid-template-rows: auto auto 1fr auto;
    grid-column: span 2;
  }
 
  .product-image {
    grid-row: 1 / -1;
    aspect-ratio: auto;
    height: 100%;
  }
}

Building a Dashboard with Subgrid

Dashboard widgets often need aligned headers, content areas, and footers across a grid. Subgrid makes this straightforward without JavaScript or fixed heights.

/* Dashboard grid */
.dashboard {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto auto;
  gap: 1.5rem;
}
 
/* Stats card with subgrid alignment */
.stat-card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  background: white;
  border-radius: 0.75rem;
  padding: 1.5rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
 
/* All stat values align horizontally */
.stat-value {
  font-size: 2.5rem;
  font-weight: 700;
}
 
/* All stat labels align horizontally */
.stat-label {
  font-size: 0.875rem;
  color: #6b7280;
}
 
/* All trend indicators align horizontally */
.stat-trend {
  font-size: 0.875rem;
}

Combining All Three Features

A production design system combines all three modern CSS features. Cascade layers organize styles by precedence. Container queries make components responsive to their context. Subgrid aligns nested content across sibling elements.

/* Layer organization */
@layer tokens, reset, base, components, utilities;
 
@layer tokens {
  :root {
    --color-primary: #2563eb;
    --space-4: 1rem;
    --radius-md: 0.5rem;
  }
}
 
@layer components {
  /* Product card uses container queries */
  .product-card-wrapper {
    container-type: inline-size;
  }
 
  .product-card {
    display: grid;
    grid-template-rows: auto 1fr auto;
    gap: var(--space-4);
    border-radius: var(--radius-md);
    background: white;
  }
 
  @container (min-width: 400px) {
    .product-card {
      grid-template-columns: 200px 1fr;
      grid-template-rows: auto auto 1fr auto;
    }
  }
 
  /* Product grid uses subgrid for alignment */
  .product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    grid-template-rows: subgrid;
    gap: var(--space-4);
  }
}
 
@layer utilities {
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; }
}

Modern CSS implementation

Real-World Use Cases

Design System Component Library

A design system with components that work in any layout context benefits enormously from container queries. A card component that adapts to its container width works identically in a full-width layout, a sidebar, a modal, and a split view without any container-specific CSS. Cascade layers ensure that component styles always override base styles, and utility classes always override component styles, without specificity hacks.

E-Commerce Product Grid

Product grids where cards have varying content lengths benefit from subgrid. Product titles, descriptions, prices, and action buttons align across cards regardless of content length, creating a polished, professional layout. Container queries ensure the cards adapt correctly when the product grid is placed in different layout contexts (main content, sidebar, modal).

Dashboard with Responsive Widgets

Dashboards contain widgets that must adapt to varying container sizes. Container queries let each widget respond to its own container's dimensions. Subgrid ensures that widget headers, content areas, and footers align across the grid. Cascade layers organize the dashboard's style hierarchy so that widget-specific styles do not conflict with the dashboard framework's styles.

Third-Party Style Integration

Applications that use third-party component libraries alongside custom styles benefit from cascade layers. Third-party styles go in a vendor layer, custom component styles in a components layer, and utility overrides in a utilities layer. Specificity conflicts are resolved by layer order, not by selector complexity.

Best Practices for Production

  1. Use container queries instead of media queries for components: Container queries make components truly portable. They work correctly regardless of where they are placed in the layout.

  2. Declare container-type on the parent, not the element being styled: The containment context must be on an ancestor of the element you want to style.

  3. Use subgrid for alignment across sibling elements: When you need titles, descriptions, or actions to align across cards or rows, subgrid is the correct tool.

  4. Organize styles into logical cascade layers: Group styles by their role (reset, base, components, utilities) and declare the layer order once at the top of your stylesheet.

  5. Use inline-size containment, not size: Setting container-type: size makes the element a containment context in both dimensions, which can break layouts. Use inline-size for most cases.

  6. Name your containers: Using container-name makes your @container queries more readable and prevents accidental matches against unrelated containers.

  7. Use container query units for fluid scaling: cqi and cqb units enable fluid typography and spacing that scales with the container, not the viewport.

  8. Import third-party CSS into layers: Use @import url("...") layer(vendor) to ensure vendor styles never override your custom styles.

  9. Test in all major browsers: Container queries, subgrid, and cascade layers are supported in all modern browsers, but always verify your specific use case.

  10. Use feature queries for progressive enhancement: Wrap modern CSS features in @supports blocks for graceful degradation in older browsers.

Common Pitfalls and Solutions

PitfallImpactSolution
Using container-type: size unnecessarilyBreaks height-dependent layoutsUse inline-size unless you need both dimensions
Forgetting container-type on parentContainer queries don't workAlways declare containment context first
Using subgrid without grid-row: span NChild grid has no tracks to inheritAlways specify how many tracks the subgrid spans
Not declaring layer orderLayers have no precedenceDeclare @layer order at the top of the stylesheet
Overusing !important with layersDefeats the purpose of layersUse layer ordering instead of !important
Nesting @container queries deeplyConfusing, hard to maintainKeep queries flat and simple
Placing overrides in a low layerOverrides don't workUnlayered styles have highest precedence; use an overrides layer
Using subgrid in only one axis when both need alignmentPartial alignmentUse subgrid for both columns and rows when needed

Migration Strategy

Adopting modern CSS features does not require a rewrite. Take an incremental approach:

  1. Start with cascade layers: Organize your existing stylesheets into layers. This provides immediate benefits with no visual changes. Wrap existing styles in @layer base { ... } and create a utilities layer for utility classes.

  2. Add container queries to key components: Identify components that use multiple media queries for viewport-dependent responsiveness. Replace those media queries with container queries. This makes the components portable across layout contexts.

  3. Refactor grid layouts to use subgrid: Identify grids where nested elements need to align across siblings. Replace the nested grid with a subgrid that inherits the parent's tracks.

  4. Integrate third-party CSS into layers: Import third-party stylesheets into a vendor layer to prevent specificity conflicts with your custom styles.

This incremental approach lets you adopt each feature independently and measure the impact on both developer experience and user experience.

CSS Nesting

Native CSS nesting eliminates the need for preprocessors like Sass for basic nesting. Combined with cascade layers and container queries, native CSS now covers most use cases that previously required a build step.

/* Native CSS nesting — no preprocessor needed */
.card {
  background: white;
  border-radius: 0.75rem;
 
  & .card-header {
    padding: 1rem;
    border-bottom: 1px solid #e5e7eb;
 
    & h2 {
      font-size: 1.25rem;
      margin: 0;
    }
  }
 
  & .card-body {
    padding: 1rem;
 
    & p {
      line-height: 1.6;
      color: #374151;
    }
  }
 
  /* Media queries inside nesting */
  @media (min-width: 768px) {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

The :has() Selector

The :has() selector is often called the "parent selector" because it lets you style an element based on its descendants. This was previously impossible in CSS without JavaScript.

/* Style a form group differently when it contains an invalid input */
.form-group:has(input:invalid) {
  border-color: #ef4444;
  background-color: #fef2f2;
}
 
/* Style a card differently when it contains an image */
.card:has(img) {
  grid-template-rows: 200px 1fr;
}
 
/* Style a nav item when its sibling is active */
.nav-item:has(+ .nav-item.active) {
  border-right-color: transparent;
}
 
/* Complex: style a page layout based on content */
.page:has(.sidebar) {
  display: grid;
  grid-template-columns: 250px 1fr;
}
 
.page:not(:has(.sidebar)) {
  max-width: 800px;
  margin: 0 auto;
}

CSS Nesting + Container Queries + :has() Together

These features combine to create powerful, pure-CSS responsive components:

/* A card that responds to its container AND its content */
@layer components {
  .card {
    container-type: inline-size;
    background: white;
    border-radius: 0.75rem;
 
    /* Default: stacked layout */
    & .card-content {
      display: grid;
      gap: 1rem;
    }
 
    /* Wide container: side-by-side layout */
    @container (min-width: 400px) {
      & .card-content {
        grid-template-columns: 200px 1fr;
      }
    }
 
    /* Has image: add visual emphasis */
    &:has(img) {
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 
      & .card-content {
        padding: 0;
      }
    }
 
    /* No image: text-focused styling */
    &:not(:has(img)) {
      border: 1px solid #e5e7eb;
 
      & .card-content {
        padding: 1.5rem;
      }
    }
  }
}

Conclusion

Modern CSS features represent a paradigm shift in how we write styles:

  1. Container queries make components truly portable: A component that responds to its own container's size works correctly in any layout context—sidebar, main content, modal, or email.

  2. Subgrid enables true cross-element alignment: Child elements in different cards or rows can align with each other, creating polished layouts that were previously impossible without JavaScript.

  3. Cascade layers eliminate specificity wars: Explicit layer ordering replaces the arms race of increasingly specific selectors and !important declarations.

  4. These features are production-ready: All three features have baseline browser support. You can use them today without polyfills or fallbacks.

  5. They reduce JavaScript dependency: Layout logic that previously required JavaScript (ResizeObserver, dynamic class toggling) can now be handled entirely in CSS.

  6. They align with component-driven architecture: Modern CSS features are designed for the component model that React, Vue, and Svelte use. They make component styling more intuitive and more maintainable.

If you are still writing CSS with only media queries, viewport units, and specificity hacks, adopting these modern features will transform your stylesheets. The code is cleaner, the layouts are more robust, and the components are more portable. Start with cascade layers for immediate organizational benefits, add container queries for component responsiveness, and use subgrid where cross-element alignment matters. Together, these features define the modern CSS development experience.