Karya Semi
HomeBlogSearchCategoriesAboutContact
Karya Semi

Less noise. More notes.

HomeBlogAboutContactPrivacy PolicyDisclaimer

© 2026 Karya Semi. All rights reserved.

XGitHubLinkedIn
  1. Home
  2. /Categories
  3. /Web Development

Building Responsive UI Components with CSS Container Queries

Discover how css container queries responsive design lets you build modular components that adapt to their parent element size rather than the viewport.

Dian Rijal Asyrof/August 14, 2026/8 min read
Illustration for Building Responsive UI Components with CSS Container Queries

For a long time, responsive web design meant one thing: looking at the viewport. We wrote media queries that checked the width of the browser window and rearranged the entire page layout based on that single number. It worked well when layouts were simple. But as frontend development shifted toward modular, component-based architectures, viewport-based media queries started to show their limits.

Think about a standard card component. It has an image, a heading, some description text, and a button. On a mobile screen, you want it stacked vertically. On a wide desktop screen, you want it laid out horizontally to save vertical space. If you use media queries, you write code that switches the layout when the viewport hits 768px.

But what happens when you need to put that same card inside a narrow sidebar on a desktop screen? The viewport is 1440px wide, so the media query triggers the horizontal layout. The card tries to stretch out inside a 300px sidebar, breaking your design completely.

We used to solve this by creating class modifiers. We wrote classes like .card-sidebar or .card-horizontal and manually applied them depending on where the component lived. It required developers to know the context of the component beforehand. It made the CSS complex and hard to maintain.

Some developers turned to JavaScript. They used ResizeObserver to monitor the size of the parent element and toggle classes dynamically. While this worked, it introduced performance costs. JavaScript has to run, measure the DOM, and apply classes, which often causes layout shifts and flashes of unstyled content. To prevent these visual stutters, developers often focus on optimizing Core Web Vitals to ensure a smooth user experience.

CSS Container Queries solve this by allowing us to style an element based on the size of its parent container rather than the viewport. If a container is small, the component renders in its compact form. If the container grows, the component adapts to use the extra space. The component becomes truly modular and context-aware.

How Container Queries Work

To use container queries, you need to define a container element and then query that container from a child element.

Let's look at the syntax. First, define the containment context on the parent element using the container-type property:

.sidebar, .main-content {
  container-type: inline-size;
}

The container-type property tells the browser to monitor this element's dimensions. We use inline-size because we want to query the width of the container. In horizontal writing modes, inline-size corresponds to width. You can also use size to monitor both width and height, but this is rarely needed and can cause layout loops if the height of the container depends on the height of its children.

Once the parent is defined as a container, you can write a container query for any element inside it:

@container (min-width: 450px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
    gap: 1rem;
  }
}

This query looks up the DOM tree to find the nearest ancestor with a defined container-type. If that ancestor is at least 450px wide, the grid styles apply. If it is smaller, the default styles apply.

Pattern 1: The Adaptive Profile Card

Let's write the HTML and CSS for a card component that changes layout based on its container size.

<div class="component-wrapper">
  <div class="profile-card">
    <img src="avatar.jpg" alt="User Avatar" class="profile-card__avatar">
    <div class="profile-card__info">
      <h3 class="profile-card__name">Alex Rivera</h3>
      <p class="profile-card__role">Senior Systems Engineer</p>
      <p class="profile-card__bio">Building reliable frontend architectures and writing clean, maintainable CSS systems.</p>
    </div>
  </div>
</div>

Now, let's write the CSS. We want the card to be stacked in narrow spaces, go side-by-side in medium spaces, and show an expanded layout in wide spaces.

.component-wrapper {
  container-type: inline-size;
}
 
.profile-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 1.5rem;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  background-color: #ffffff;
  text-align: center;
}
 
.profile-card__avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
}
 
/* Medium container layout */
@container (min-width: 450px) {
  .profile-card {
    flex-direction: row;
    text-align: left;
    padding: 2rem;
  }
 
  .profile-card__avatar {
    width: 100px;
    height: 100px;
  }
}
 
/* Wide container layout */
@container (min-width: 700px) {
  .profile-card {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2rem;
  }
 
  .profile-card__avatar {
    width: 120px;
    height: 120px;
  }
 
  .profile-card__info {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 1rem;
  }
}

This setup allows the card to adapt smoothly. If you drop this component into a sidebar, a main content area, or a grid column, it will automatically render the correct layout without any extra class modifiers.

Pattern 2: The Responsive Dashboard Widget

Let's look at another common pattern: a dashboard widget that displays system statistics. In a small container, we want a simple list of values. In a medium container, we want a two-column layout. In a wide container, we want a four-column grid with visual cards.

<div class="widget-container">
  <div class="stat-widget">
    <h4 class="stat-widget__title">System Status</h4>
    <div class="stat-widget__grid">
      <div class="stat-item">
        <span class="stat-item__label">CPU Usage</span>
        <span class="stat-item__value">42%</span>
      </div>
      <div class="stat-item">
        <span class="stat-item__label">Memory</span>
        <span class="stat-item__value">8.2 GB</span>
      </div>
      <div class="stat-item">
        <span class="stat-item__label">Disk I/O</span>
        <span class="stat-item__value">120 MB/s</span>
      </div>
      <div class="stat-item">
        <span class="stat-item__label">Network</span>
        <span class="stat-item__value">45 Mbps</span>
      </div>
    </div>
  </div>
</div>

Here is the CSS to drive the layouts:

.widget-container {
  container-type: inline-size;
}
 
.stat-widget {
  padding: 1.25rem;
  background: #f8fafc;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
}
 
.stat-widget__title {
  margin-bottom: 1rem;
  font-size: 1.1rem;
  color: #1e293b;
}
 
.stat-widget__grid {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
 
.stat-item {
  display: flex;
  justify-content: space-between;
  padding: 0.5rem 0;
  border-bottom: 1px solid #e2e8f0;
}
 
/* Medium container: Switch to two columns */
@container (min-width: 380px) {
  .stat-widget__grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
  }
 
  .stat-item {
    flex-direction: column;
    justify-content: flex-start;
    border-bottom: none;
    background: #ffffff;
    padding: 0.75rem;
    border-radius: 6px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  }
}
 
/* Wide container: Switch to four columns */
@container (min-width: 650px) {
  .stat-widget__grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

The widget adjusts itself depending entirely on the width allocated to it. This makes designing complex dashboard layouts much simpler because the parent grid can change layout without breaking the child widgets.

Pattern 3: The Adaptive Form Layout

Form layouts are notoriously difficult to manage in responsive design. A checkout form might sit in a wide main area on one page, but inside a narrow modal or sidebar on another. While you can use libraries like Jelly UI for native HTML form controls to add expressive physics-based interactions, managing the layout itself has historically required complex CSS. With container queries, we can make the form layout adapt based on the width of its wrapper.

<div class="form-container">
  <form class="checkout-form">
    <div class="form-group">
      <label for="first-name">First Name</label>
      <input type="text" id="first-name">
    </div>
    <div class="form-group">
      <label for="last-name">Last Name</label>
      <input type="text" id="last-name">
    </div>
    <div class="form-group full-width">
      <label for="email">Email Address</label>
      <input type="email" id="email">
    </div>
    <div class="form-group city">
      <label for="city">City</label>
      <input type="text" id="city">
    </div>
    <div class="form-group state">
      <label for="state">State</label>
      <input type="text" id="state">
    </div>
    <div class="form-group zip">
      <label for="zip">Zip Code</label>
      <input type="text" id="zip">
    </div>
  </form>
</div>

And the CSS to handle the transitions:

.form-container {
  container-type: inline-size;
}
 
.checkout-form {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  padding: 1.5rem;
  background: #ffffff;
}
 
.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}
 
.form-group input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #cbd5e1;
  border-radius: 4px;
}
 
/* Medium container: Switch to a two-column grid */
@container (min-width: 500px) {
  .checkout-form {
    grid-template-columns: repeat(2, 1fr);
  }
 
  .full-width {
    grid-column: span 2;
  }
}
 
/* Wide container: Switch to a multi-column layout for details */
@container (min-width: 750px) {
  .checkout-form {
    grid-template-columns: repeat(6, 1fr);
  }
 
  .form-group {
    grid-column: span 3;
  }
 
  .full-width {
    grid-column: span 6;
  }
 
  .city {
    grid-column: span 3;
  }
 
  .state {
    grid-column: span 2;
  }
 
  .zip {
    grid-column: span 1;
  }
}

If this form is squeezed into a small panel, it falls back to a clean, single-column layout. When it gets more breathing room, it expands to two columns, and eventually to a full grid where city, state, and zip code sit neatly on a single line.

Naming Containers for Complex Layouts

By default, a container query targets the nearest parent container. But sometimes you have nested containers and you need a component to respond to a specific container higher up the tree.

To handle this, we can name our containers using the container-name property, or combine it into the container shorthand.

.page-layout {
  container: main-layout / inline-size;
}
 
.card-wrapper {
  container: card-container / inline-size;
}

Now we can target these containers specifically:

/* Query the card container */
@container card-container (min-width: 300px) {
  .card-title {
    font-size: 1.2rem;
  }
}
 
/* Query the main layout container */
@container main-layout (min-width: 900px) {
  .card-title {
    font-size: 1.5rem;
    color: #0f172a;
  }
}

This gives you precise control over which container influences which styles, preventing conflicts in nested component structures.

Container Query Units

Container queries also introduce a new set of CSS units that scale relative to the size of the container:

  • cqw: 1% of the container's width.
  • cqh: 1% of the container's height.
  • cqi: 1% of the container's inline size (width in horizontal layouts).
  • cqb: 1% of the container's block size (height in horizontal layouts).
  • cqmin: The smaller value of cqi or cqb.
  • cqmax: The larger value of cqi or cqb.

These units are useful for fluid typography. If you want a heading to scale smoothly based on how wide the container is, you can use cqi inside a clamp() function:

.hero-banner {
  container-type: inline-size;
}
 
.hero-banner__title {
  font-size: clamp(1.75rem, 6cqi, 4rem);
}

Using clamp() ensures that the text stays within readable limits, even if the container becomes extremely small or extremely large. It prevents the text from blowing up or shrinking to an unreadable size.

Common Pitfalls to Avoid

While container queries are powerful, they have a few rules that can trip you up.

First, you cannot style the container itself inside a container query that targets that container. For example, this will not work:

.my-container {
  container-type: inline-size;
}
 
@container (min-width: 500px) {
  .my-container {
    background-color: red; /* This fails */
  }
}

The browser cannot query an element's size to change the layout or style of that same element, as this could easily create an infinite rendering loop. You must style the children of the container, or query a parent container to style the child.

Second, be careful with margins and padding on the container. If you change the padding of a container based on its own width, it can change the content width, triggering another layout recalculation. Stick to querying the container to style internal elements.

Browser Support and Fallbacks

Container queries are supported in all major modern browsers (Chrome, Edge, Safari, Firefox) since early 2023. They are a core part of the modern layout capabilities highlighted in CSS Baseline 2026 browser features. Unless you need to support old versions of Internet Explorer or legacy mobile browsers, you can use them safely today.

If you must provide a fallback, build your components mobile-first. The default CSS should represent the narrowest, most basic layout. Browsers that do not understand container queries will ignore the @container rules and render the default layout, which is still perfectly functional.

You can also wrap your container query styles in a @supports rule to prevent old browsers from parsing them:

.card {
  /* Default mobile-first styles */
  display: block;
}
 
@supports (container-type: inline-size) {
  .card-container {
    container-type: inline-size;
  }
 
  @container (min-width: 500px) {
    .card {
      display: flex;
    }
  }
}

This ensures a clean, progressive enhancement strategy.

The Mental Shift

Using container queries requires a shift in how we think about responsive design. Instead of asking "How does this page look on a tablet?", we ask "How does this component look when it has 400px of space?".

This makes our UI libraries much more robust. We can build a component once, test it at various widths, and know it will look correct wherever it is placed. It decouples the layout of the page from the design of the individual components.

DR

Dian Rijal Asyrof

Writes about useful AI tools, programming practice, and the craft of building reliable software.

Previous articleUnderstanding Multitenant Database Index Tuning Strategies in PostgreSQL
CSSWeb DevelopmentFrontend
On this page↓
  1. How Container Queries Work
  2. Pattern 1: The Adaptive Profile Card
  3. Pattern 2: The Responsive Dashboard Widget
  4. Pattern 3: The Adaptive Form Layout
  5. Naming Containers for Complex Layouts
  6. Container Query Units
  7. Common Pitfalls to Avoid
  8. Browser Support and Fallbacks
  9. The Mental Shift

On this page

  1. How Container Queries Work
  2. Pattern 1: The Adaptive Profile Card
  3. Pattern 2: The Responsive Dashboard Widget
  4. Pattern 3: The Adaptive Form Layout
  5. Naming Containers for Complex Layouts
  6. Container Query Units
  7. Common Pitfalls to Avoid
  8. Browser Support and Fallbacks
  9. The Mental Shift

See also

Illustration for Offloading Heavy Computations with Web Workers in Modern JavaScript
Web Development/Aug 13, 2026

Offloading Heavy Computations with Web Workers in Modern JavaScript

Keep your UI responsive by running CPU-heavy tasks in the background. Learn how to implement javascript web workers to boost performance and prevent page freezing.

7 min read
Web DevelopmentJavaScript
Illustration for Vite 8 Migration Guide: Breaking Changes and Upgrade Checklist
Web Development/Aug 8, 2026

Vite 8 Migration Guide: Breaking Changes and Upgrade Checklist

A complete step-by-step developer playbook to upgrade your project to Vite 8, handle the Rolldown/Oxc transition, and fix breaking changes.

7 min read
ViteFrontend
Illustration for Jane Street Built a UI Library in OCaml, Web Developers Should Pay Attention
Web Development/Aug 4, 2026

Jane Street Built a UI Library in OCaml, Web Developers Should Pay Attention

Jane Street just open-sourced Bonsai, their OCaml-based UI library. Sounds irrelevant to web devs? It's actually a signal about where frontend architecture is heading.

5 min read
Web DevelopmentFrontend