Master CSS Container Queries: A Hands-On Tutorial for 2025

I spent years of my career writing CSS that I knew, deep down, was a house of cards.

I remember one project in particular—a dashboard with a feed of “insight” cards. We had this beautiful card component. In the main, wide column, it was supposed to show a horizontal layout with an icon on the left and text on the right. In the narrow sidebar, for “related insights,” it was meant to stack vertically. Simple, right?

Of course not. The only tool I had was a media query. So I wrote something like @media (min-width: 960px). And on a big screen, it worked. The card in the main column looked great. But the one in the sidebar—which was still crammed into a 300px column—also tried to go horizontal. It was a disaster. Text overflowing, icons squished. It was broken.

My solution? A horrible, brittle override. .sidebar .card { ... }. I felt dirty writing it. I was tying my component’s style to its location, creating a dependency that I knew would come back to haunt us the next time a product manager asked to move things around.

We’ve all been there. We build these beautiful, self-contained components in React or Vue, only to style them with a global, context-blind tool that feels like a relic from another era. For years, we relied on JavaScript hacks with ResizeObserver just to make a component aware of the box it was actually in. It worked, but it always felt like we were patching a fundamental flaw in the platform.

Well, the patch is no longer needed. Container queries are here, they’re ready for production, and they fundamentally change how we should think about responsive styling. They fix the disconnect. And if you’re still reaching for location-based class names or JS workarounds, I’m here to tell you—there is a much, much better way.

A New Way of Thinking: From Global Page to Local Component

The biggest hurdle with media queries wasn’t the syntax—it was the mental model it forced on us. We had to think “top-down.” We’d ask, “What is the width of the entire page?” and then try to guess what that meant for our tiny component buried three levels deep in the DOM.

This is what I call extrinsic styling. The component’s appearance was dictated by something external to it. A change in the global layout could—and often would—break our components in unexpected ways. It was fragile by design.

Container queries flip this script entirely. They enable intrinsic styling.

“Now, the component itself gets to ask the question: “How much space do I have?” It doesn’t care about the viewport. It doesn’t care if it’s in a sidebar or a modal or a main element. It has one job: look its best in the space it’s been given. This moves layout intelligence from the global scope of the page into the local scope of the component.

This isn’t just a new feature; it’s a validation of the way we’ve already been building applications for years. Finally, our CSS architecture can match our component architecture.

The Anatomy of a Query: The Rules I Wish I Knew on Day One

Getting started is easy, but there are a couple of “gotchas” that can trip you up. Here’s what I wish someone had just laid out for me from the start.

container-type – The “On” Switch

First thing’s first: for a component to query its parent’s size, that parent must be declared a “container.” It’s a deliberate, mandatory step.

The container-type property does this. While there are a few values, 99% of the time you’ll use inline-size.

  • inline-size: This is your workhorse. It establishes a query container for width. Crucially, it allows the container’s height to be determined by its content, which is exactly what you want most of the time.
  • size: This creates a container for both width and height. Use this only when your container has an explicitly set height.
  • normal: The default. This only establishes a container for style queries, not size queries.

So why is container-type: inline-size so critical? This is the part that blew my mind when I first understood it. It solves a problem that made container queries seem impossible for years: the infinite loop paradox. Imagine if a container query changed the font-size of its child. That new font size could make the text wrap differently, which could change the container’s width, which would re-trigger the query, which would change the font size… and on and on, forever. Your browser would melt.

container-type prevents this. It tells the browser’s rendering engine, “When you calculate the width of this container, don’t base it on the size of its children.” It breaks the loop. This means the container’s size must be determined by its own context—like being a grid track or having a width set. Forgetting this is the #1 reason your queries won’t work.

container-name – Scoping for Sanity

By default, an @container query looks at the nearest parent container. In a real-world app with nested layouts, that’s a recipe for confusion. This is where container-name becomes your best friend. It lets you give your containers specific, unique names.


.sidebar {
  container-type: inline-size;
  container-name: sidebar-container;
}

.card-wrapper {
  /* You can also use a shorthand for both! */
  container: card-container / inline-size;
}

/* Now I can be explicit! */
@container card-container (width > 450px) {
  .card { ... }
}

I now consider this a mandatory best practice. As soon as I define a container, I give it a name. It makes the CSS self-documenting and saves me from future headaches.

Let’s Build It: The Right, the Wrong, and the Ugly

Talk is cheap. Let’s build that product card from my story and see the difference in action. You can play with each of these demos live.

Part I: The Brittle Past – The Media Query Way

First, the way I used to do it. The result is a broken component in the sidebar because the media query only cares about the viewport. This is the core problem.

CSS:


/* The Brittle Media Query */
@media (min-width: 960px) {
    .card {
        grid-template-columns: 140px 1fr; /* Horizontal layout */
    }
}




Codepen

See the Pen Media query by mail css (@mail-css) on CodePen.

Part II: The JavaScript Interlude – The ResizeObserver Hack

Before we had a native solution, we reached for ResizeObserver. It felt clever at the time. This version works correctly, but it always feels wrong. We’re using JavaScript to manage presentation, adding complexity and a potential flash of unstyled content. It was a hack—a necessary one, but a hack nonetheless.

CSS:


/* Media query is removed. Now we use a class. */
.card.is-horizontal {
    grid-template-columns: 140px 1fr;
}

JavaScript:


const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    // Toggle class based on the container's width
    entry.target.firstElementChild.classList.toggle('is-horizontal', entry.contentRect.width > 450);
  }
});

document.querySelectorAll('.card-wrapper').forEach(wrapper => {
  observer.observe(wrapper);
});


See the Pen JavaScript ResizeObserver hack by mail css (@mail-css) on CodePen.

Part III: The Elegant Present – The Container Query Solution

Now, let’s do it the right way. No JavaScript. No brittle overrides. Just clean, modern CSS.

HTML:

<!-- The wrapper now serves as the container -->
<div class="product-card-container">
  <article class="product-card">
    <img src="https://placehold.co/600x400/4f46e5/ffffff?text=Works!" alt="Adaptive Card">
    <div class="product-card-content">
      <h2 class="product-card-title">Adaptive Card</h2>
      <p>My layout is based on my container's width.</p>
      <button class="product-card-button">Learn More</button>
    </div>
  </article>
</div>

CSS:

/* 1. Establish the container context. */
.product-card-container {
    container: product-card / inline-size;
}

/* 2. Define default stacked layout for the card. */
.product-card {
    display: grid;
}

/* 3. Apply the new layout when the CONTAINER has space. */
@container product-card (width > 450px) {
    .product-card {
        grid-template-columns: 140px 1fr;
    }
}

See the Pen Section 3 by mail css (@mail-css) on CodePen.

That’s it. It’s declarative, incredibly performant, and the logic lives right where it belongs—with the component. It works perfectly everywhere, every time. This is the “aha!” moment.

Beyond Breakpoints: Fluid Scaling with cq Units

Once you master size queries, you’ll discover the next superpower: container query units. These are like vw (viewport width) units, but they’re scoped to the container, allowing for perfectly fluid typography and spacing.

cqi (container query inline-size), which is equal to 1% of the container’s width. What can you do with this? Fluid typography.

Instead of just changing the layout, we can make the font size of our card’s title grow and shrink smoothly with the size of the card itself.


.product-card__title {
  /* Min size, ideal scalable size, max size */
  font-size: clamp(1rem, 1rem + 2cqi, 1.5rem);
}

This one line of CSS is astonishingly powerful. The title will now scale perfectly relative to its component, creating a truly fluid design that would have been a nightmare to calculate with JavaScript.

The Cutting Edge: What I’m Experimenting With Now

Size queries are your bread and butter, but if you want to see where things are headed, you need to know about Style Queries.

This is a different beast entirely. Instead of querying size, Style Queries let you query the computed style values of a container—specifically, the value of a CSS Custom Property.

It’s like having a tiny state machine in your CSS. Imagine you want a “featured” version of your card. Instead of adding a class with JavaScript, you can just set a variable:


<div class="product-card-container" style="--featured: true">
  ...
</div>

And then your CSS can react to it directly:

/* No container-type needed for style queries! */
@container style(--featured: true) {
 .product-card {
    border: 2px solid gold;
    box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
  }
}

This is huge. It pulls even more presentational logic out of JavaScript and into CSS, where it belongs. Browser support is still growing here, so I treat this as a progressive enhancement for now.

My Production Checklist: How to Use This Without Shooting Yourself in the Foot

Container queries are production-ready in all major browsers for 2025. Here’s my simple heuristic for using them in real-world projects.

  • Media Queries for MACRO Layout: I still use media queries, but only for the big picture. Things like changing a page from one to two columns, or hiding a sidebar on mobile. These are page-level concerns.
  • Container Queries for MICRO Layout: I use container queries for everything else. Any component that needs to adapt its own layout—cards, navigation bars, widgets, you name it—gets a container query. These are component-level concerns.

And remember these key rules—the mistakes I’ve made so you don’t have to:

  • You MUST set container-type. I can’t say this enough. If your query isn’t working, this is the first thing to check.
  • A container CANNOT style itself. You need a wrapper element.
  • A container MUST have a defined size from its parent (like a grid track) or an explicit width. It can’t size itself from its children.
  • You SHOULD use container-name. In any real app, it’s not optional. It’s for your own sanity.

The Web We Always Wanted

Looking back, it’s hard to imagine building components any other way. Container queries aren’t just a new CSS feature; they’re the missing link that finally connects our styling capabilities to our modern, component-driven development models.

It allows us to build a more modular, resilient, and predictable front-end. We can create components that are truly portable, that we can drop anywhere in a layout and trust that they will Just Work™. It’s the responsive web we were always promised, and it’s finally here. It’s time to stop writing brittle CSS and start building components that are truly aware of their own context.