DEV Community

Cover image for Your card breaks in the sidebar. Here's the real fix.
Parsa Jiravand
Parsa Jiravand

Posted on

Your card breaks in the sidebar. Here's the real fix.

You built a card. Image, title, a line of copy. In the main content column it's gorgeous — image left, text right, everything breathing.

Then someone drops it into the sidebar.

Now the image is squashed next to three words per line, the title wraps four times, and the whole thing looks broken. You open DevTools, check your breakpoints, and they're all firing correctly. The viewport is wide. So why does your card look like it's having a stroke?

Here's the punchline up front: the bug isn't in your CSS. It's in the question your CSS is allowed to ask. And the fix is one line.

The question media queries can't answer

For fifteen years, the only responsive question we could ask was "how wide is the window?" That's what a media query is. Resize the viewport, cross a breakpoint, restyle.

Which works fine — right up until you build a component meant to live in more than one place.

Your card doesn't care how wide the window is. It cares how wide it is. And those two numbers have nothing to do with each other. A 1400px desktop can hand your card a 220px sidebar. The media query sees "1400px — go wide!" and lays out side-by-side in a slot that's narrower than the image.

/* The card asks the window. The window has no idea where the card is. */
@media (min-width: 600px) {
  .card { grid-template-columns: 200px 1fr; }
}
Enter fullscreen mode Exit fullscreen mode

Quick gut check before you scroll: how have you solved this before? Be honest.

If you're like most of us, the answer is one of three hacks: a compact prop threaded down through five components, a ResizeObserver wired up in JavaScript, or two near-identical card variants you swap by hand. I've shipped all three. They're all workarounds for the same missing primitive — a way for a component to ask its own container, not the window, how much room it's got.

One declaration changes the question

That primitive shipped. It's called a container query, and it's in every major browser — Chrome, Firefox, Safari, Edge — no flag, no polyfill.

You do two things. Mark a parent as a container. Query that instead of the viewport.

/* 1. Tell the parent it's a query container */
.card-wrapper {
  container-type: inline-size;
}

/* 2. The compact default — image on top */
.card {
  display: grid;
  grid-template-rows: auto 1fr;
}

/* 3. Go side-by-side only when MY container has the room */
@container (min-width: 500px) {
  .card {
    grid-template-columns: 200px 1fr;
    grid-template-rows: unset;
  }
}
Enter fullscreen mode Exit fullscreen mode

Read that @container line out loud: "when my container is at least 500px, go wide." Not the window. The container.

Now drop the exact same card into the 220px sidebar and it stays stacked, because its container is 220px. Drop it into the wide grid and it opens up. One component, two layouts, zero JavaScript, zero props. The card finally knows where it lives.

When the layout nests, name the container

Sometimes the thing you want to respond to isn't the immediate parent — it's a layout region three levels up. Name your containers and query the one you mean:

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

/* This title responds to the sidebar specifically */
@container sidebar (min-width: 300px) {
  .card-title { font-size: 1.25rem; }
}
Enter fullscreen mode Exit fullscreen mode

The old way to make a deeply nested element aware of a high-level region was a ResizeObserver, a React context, and a prop-drilling session. Here it's two declarations. That's the whole feature.

So is @media dead? No — and here's the line

This is the part people get wrong: container queries don't replace media queries. They have different jobs, and mixing them up is its own mess.

  • Media query → page-level structure. One column vs. two. Collapsing the nav. A full-bleed hero. Anything that genuinely answers to the viewport — including print and prefers-reduced-motion.
  • Container query → anything reusable that gets dropped into varying space. Cards, tables, form fields, media objects.

The heuristic that's never failed me: the layout skeleton is a media query; everything you hang on it is a container query.

The bonus you'll notice in Storybook

Here's a side effect I didn't expect. Build a card with media queries inside it and your Storybook preview lies — it shows the component at the story frame's viewport width, not the width it'll get in production. The preview looks fine; the real layout breaks.

Swap to container queries and the preview tells the truth. Resize the story panel and the card adapts, because "context" now lives in the CSS instead of being inferred from the window. What you see in isolation is what you ship.

The fix was one line — and a better question

Remember the broken sidebar card? container-type: inline-size on its wrapper. That's it. The layout you already wrote starts working the moment the card can see its own container instead of guessing from the window.

The takeaway you can repeat to a teammate tomorrow: stop asking "how wide is the window?" and start asking "how wide am I?" Once the question changes, the component designs itself.

So — how many compact props are still threaded through your codebase right now? Go count. I bet most of them want to be a single @container rule. Tell me what you find in the comments.

Top comments (0)