loke.dev
Header image for 4 UI Patterns That Are Only Possible With CSS Container Queries

4 UI Patterns That Are Only Possible With CSS Container Queries

Stop using the viewport as a proxy for component size and start building modular UI that actually handles its own responsiveness.

· 4 min read

Why are we still pretending that the width of a user's browser window has anything to do with how a comment box should look inside a narrow sidebar?

For years, we’ve been using @media queries as a proxy for component layout. We’ve all been there: you build a beautiful "Card" component that looks great on the main feed, but then you drop it into a right-hand sidebar and everything breaks. To fix it, you end up writing hacky override classes like .card--sidebar or .card--small.

It’s a mess. It’s fragile. And honestly, it’s a bit of a lie. You don't care if the screen is 1200px wide; you care if the *parent container* has enough room to breathe.

Container queries changed the game. They allow a component to own its own responsiveness. Here are four UI patterns that were essentially impossible (or incredibly painful) to build before container queries.

1. The "Chameleon" Card

The most common use case is a card that needs to look different depending on where it lives. Imagine a product card: in a large 3-column grid, it should be a vertical stack. In a narrow sidebar, it should also be a vertical stack. But in a wide "Featured" section, it should be a horizontal row with a large image.

With @media queries, you'd need a dozen different class permutations. With container queries, the card just looks at its available space.

/* First, tell the parent to be a container */
.card-wrapper {
  container-type: inline-size;
}

/* Now, the card defines its own destiny */
.card {
  display: flex;
  flex-direction: column;
}

@container (min-width: 450px) {
  .card {
    flex-direction: row;
    align-items: center;
    gap: 2rem;
  }
}

The "Why": This allows you to drop the .card anywhere—a modal, a sidebar, a footer—and it will *always* choose the best layout for its environment. No "contextual" CSS required.

2. The Context-Aware Search Bar

I love this pattern for dashboards. You have a search input that lives in a header. When the header is wide, you want a "Search" label and maybe some "Quick Filter" buttons. When the header gets squished (perhaps because the user opened a side panel), you want to hide the labels and turn the buttons into icons.

Before, you'd have to sync the state of your sidebar with the state of your header in JavaScript just to toggle a class. Now?

.search-container {
  container-type: inline-size;
}

.search-label {
  display: none;
}

@container (min-width: 300px) {
  .search-label {
    display: inline;
  }
}

@container (min-width: 500px) {
  .extra-filters {
    display: flex;
  }
}

The Gotcha: Be careful not to set the container-type on the element you are trying to hide. You can't query an element about its own size to change its own size (that’s an infinite loop waiting to happen). Always query the parent.

3. The "Self-Stacking" Form Group

Forms are a layout nightmare. You usually want a label next to an input (label { width: 30% }), but if the form is in a narrow column, that looks terrible.

The problem? You might have a two-column form on desktop where each column is actually quite narrow. A @media query would see "Desktop" and try to put the labels side-by-side, causing the inputs to be 20px wide.

.form-row {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.form-field {
  width: 100%;
}

@container (min-width: 400px) {
  .form-field {
    display: grid;
    grid-template-columns: 150px 1fr;
    align-items: center;
  }
}

By using @container, the form row decides to stack the labels only when the *row itself* is too narrow, regardless of whether the user is on a 27-inch iMac or an iPhone.

4. Modular Typography Scaling

This is my favorite "secret" pattern. Usually, we scale font sizes based on the viewport (rem or vw). But if you have a "Hero" component, you want the font to be huge when the hero is full-width, and smaller if that same hero is used in a smaller sub-section of the page.

Container queries introduced new units: cqw (1% of container width), cqh, cqmin, and cqmax.

.hero-container {
  container-type: inline-size;
}

.hero-title {
  /* Scale the font based on the container size, 
     but keep it within a sensible range */
  font-size: clamp(1.5rem, 8cqw, 4rem);
}

The Benefit: If you move that Hero from the top of the homepage into a "Special Offer" box in the sidebar, the text scales down perfectly without you writing a single line of extra CSS. It’s truly fluid typography.

Wrapping Up

The shift from "How big is the screen?" to "How much room do I have?" is the biggest mental hurdle in modern CSS.

If you're starting a new project, stop reaching for @media as your default. Try wrapping your major layout sections in container-type: inline-size and let your components decide how they want to live. Your future self—the one who doesn't have to debug 50 different .sidebar-contact-form-button classes—will thank you.