# Beste UI

> Beautiful, accessible blocks, pieces and components for shadcn/ui and Tailwind CSS, built for React and Next.js. Install one with the shadcn CLI and the code is yours to edit: no runtime dependency, nothing to upgrade.

Markdown rendition of https://ui.beste.co/blog/container-queries-for-blocks. Every page on the site has one: add `.md` to any address, or send `Accept: text/markdown`.

Last updated: 2026-07-03

Whole catalog for LLMs: https://ui.beste.co/llms.txt. Registry index: https://ui.beste.co/r/registry.json

---
[All posts](/blog)

July 3, 2026·7 min read

# Container Queries Make Section Components Honest

Viewport breakpoints answer the wrong question: they tell a component how wide the screen is, not how much room it actually has. Here's the full container queries workflow in Tailwind v4, the containment gotchas nobody mentions, and a rule for when md: is still correct.

tailwindcsscontainer-queries

![zieg](/_next/image?url=%2Fassets%2Fimages%2Fzieg.jpg&w=96&q=75&dpl=dpl_CY2dAqud5esqh3XyqpW5XSreDKej)

ziegbuilding beste.co

[zieg's website](https://zieg.beste.co)[zieg on X](https://x.com/forwardset)[zieg on LinkedIn](https://linkedin.com/in/ziegfiroyt)

Every `md:grid-cols-3` in a reusable component is a small lie waiting for the right layout to expose it.

The lie goes like this: `md:` means "when the viewport is at least 768px wide". But what the component actually wants to know is "do _I_ have at least \~700px to work with". Those are the same question exactly as long as the component spans the full page. The moment someone drops it into a sidebar, a modal, a split view, or a dashboard panel, they diverge: the viewport is 1400px, `md:` fires, and your three-column grid crams itself into a 320px rail, one word per line.

For years we accepted this because CSS gave us nothing better. That excuse expired: container queries have shipped in every major browser since early 2023, and Tailwind v4 supports them in core, no plugin. This post is the complete workflow, including the sharp edges.

### The two-line mechanic

Container queries need two participants: an ancestor that declares itself measurable, and descendants that ask about it.

cssCopy

```
/* 1. The ancestor opts in */
.card-slot {
  container-type: inline-size;
}

/* 2. Descendants query the nearest container, not the viewport */
@container (min-width: 28rem) {
  .stats { grid-template-columns: repeat(3, 1fr); }
}
```

In Tailwind v4 that's the `@container` utility on the parent and `@`\-prefixed variants on the children:

tsxCopy

```
<div className="@container">
  <dl className="grid grid-cols-1 gap-4 @md:grid-cols-3">
    <Stat label="Deploys" value="1,204" />
    <Stat label="Uptime" value="99.98%" />
    <Stat label="p95" value="212ms" />
  </dl>
</div>
```

`@md:` here means "when the _container_ is at least 28rem", regardless of what the viewport is doing. Same component, honest in a sidebar, honest full-width.

The container scale mirrors the sizing scale you already know:

| Variant | Min width     |  | Variant      | Min width      |
| ------- | ------------- |  | ------------ | -------------- |
| @3xs    | 16rem (256px) |  | @2xl         | 42rem (672px)  |
| @2xs    | 18rem (288px) |  | @3xl         | 48rem (768px)  |
| @xs     | 20rem (320px) |  | @4xl         | 56rem (896px)  |
| @sm     | 24rem (384px) |  | @5xl         | 64rem (1024px) |
| @md     | 28rem (448px) |  | @6xl         | 72rem (1152px) |
| @lg     | 32rem (512px) |  | @7xl         | 80rem (1280px) |
| @xl     | 36rem (576px) |  | @min-\[...\] | arbitrary      |

Two things worth internalizing. First, `@md` (28rem container) is a much smaller threshold than `md:` (48rem viewport); when refactoring, re-derive your breakpoints from the component's actual needs instead of transliterating. Second, `@max-md:` and range stacking (`@md:@max-xl:flex-row`) exist, so the full min/max vocabulary carries over.

### Refactoring a real section

Here's the shape of the change on a feature-card grid. Before, viewport guesses; after, container truth:

diffCopy

```
- export function FeatureGrid({ features }: FeatureGridProps) {
-   return (
-     <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
+ export function FeatureGrid({ features }: FeatureGridProps) {
+   return (
+     <div className="@container">
+       <div className="grid grid-cols-1 gap-6 @xl:grid-cols-2 @4xl:grid-cols-3">
        {features.map((f) => (
          <FeatureCard key={f.title} {...f} />
        ))}
+       </div>
      </div>
    );
  }
```

Notice the extra wrapper. It isn't stylistic. A container query can only be answered by an _ancestor_: an element can't query its own size, because its size might depend on the very styles the query would change (a circular dependency the spec forbids). The `@container` div measures; the grid inside asks. If you take one habit from this post, it's this pairing.

The cards themselves can go further. A card that's sometimes in a 3-up grid and sometimes alone in a sidebar can restructure _itself_:

tsxCopy

```
export function FeatureCard({ icon: Icon, title, body }: FeatureCardProps) {
  return (
    <div className="@container rounded-xl border bg-card p-6">
      {/* Stacked when narrow, icon-beside-text when the card itself is wide */}
      <div className="flex flex-col gap-4 @sm:flex-row @sm:items-start">
        <Icon className="size-6 shrink-0 text-primary" />
        <div>
          <h3 className="font-semibold">{title}</h3>
          <p className="mt-1 text-sm text-muted-foreground">{body}</p>
        </div>
      </div>
    </div>
  );
}
```

Containers nest freely. The grid queried the section's slot; the card queries the grid cell it landed in. Each layer answers for itself, which is precisely what "reusable" was always supposed to mean.

When two containers are in play and you need to skip past the nearest one, name them:

tsxCopy

```
<aside className="@container/sidebar">
  <div className="@container/widget">
    {/* Queries the widget by default; the sidebar when asked by name */}
    <p className="@lg/sidebar:text-base text-sm">...</p>
  </div>
</aside>
```

### The gotchas, because there are real ones

Container queries are not free. `container-type: inline-size` applies **containment**, and containment has behavioral consequences that will surprise you at 6pm on a Friday.

**Shrink-wrapped containers collapse.** Inline-size containment means the element's width may not depend on its contents (the browser sizes it as if it were empty, to break the circular dependency). For block-level elements that fill their parent, nothing changes. But put `@container` on something sized by its content (an `inline-block`, a `w-fit` chip, a flex item with content-based basis) and it collapses toward zero width. Rule of thumb: containers should get their width from their parent, never from their children.

**Fixed and absolute children get re-parented.** Containment establishes the element as the containing block for positioned descendants. A `position: fixed` dropdown inside a container no longer positions against the viewport; it positions against the container. If a menu, tooltip, or dialog inside your component suddenly renders in the wrong place after you added `@container`, this is why. Portal-based components (anything rendering through Radix's Portal) are immune, because their DOM lives outside the container entirely. Hand-rolled `fixed` positioning is not.

Audit before you add @container

Before converting a section, grep it for `fixed` and `absolute`. Positioned elements that escape upward (dropdown panels, floating badges pinned to the viewport) need either a portal or a rethink. This is the single most common breakage in real conversions, and it fails silently: no error, just a menu opening inside a card.

**Container units are the bonus prize.** Once an ancestor is a container, descendants can use `cqw`, `cqi`, `cqmin` units: percentages of the container's size. This unlocks genuinely fluid internals, like type that scales with the card instead of stepping at breakpoints:

tsxCopy

```
{/* Scales smoothly from 1rem toward 1.5rem as the container grows */}
<h3 className="text-[clamp(1rem,4cqi,1.5rem)] font-semibold">{title}</h3>
```

Used sparingly (display numbers, stat values, decorative numerals), this removes whole tiers of breakpoint fiddling.

### The iframe nuance, or why previews lie less than you think

Here's a subtlety worth knowing if you evaluate component libraries. The live previews on this site (including the embeds in these posts) render each block inside an iframe. An iframe gets its _own viewport_: `md:` inside the frame responds to the frame's width, not your monitor's. So in an iframe, viewport breakpoints accidentally behave exactly like container queries, and resizing the frame exercises a block's full responsive range honestly:

Service Grid With Icons

[View block](/block/feature3)

Loading preview…

Inside this frame, the frame is the viewport. Viewport breakpoints tell the truth here; they'd lie in a sidebar div on your page.

The trap is the asymmetry: the preview was honest, but your sidebar `<div>` is not an iframe. The block that stacked beautifully at every frame width will still consult your full-screen viewport the moment you embed it in a narrow slot. The preview didn't lie; the units did. Container queries close that gap for real.

### When viewport breakpoints are still correct

Container queries are not a migration; they're a scoping decision. The honest rule:

* **Page-level sections** (a hero, a full-width footer, a navbar) span the viewport by definition. For them, `md:` and container queries are equivalent, and `md:` is simpler. Keep it.
* **Anything that can be placed** (cards, stat rows, media objects, widgets, form layouts, the _internals_ of sections that might live in split layouts) should query its container, because its width is a decision someone else makes later.
* **Page chrome tied to the device** (mobile nav triggers, safe-area handling, touch affordances) is genuinely about the viewport. Container queries would be the wrong tool.

One sentence version: breakpoints for the page, containers for the pieces.

That split is also how you retrofit incrementally. You don't rewrite sections; you add `@container` to the slots in _your_ layout that have non-obvious widths (sidebars, panels, preview panes), then convert the components you actually place there, leaf-first. Each conversion is local, testable by dragging one divider, and independent of the rest of the page.

Blocks you install from a registry are source in your repo, so this refactor is available to you on day one: open the file, add the wrapper, swap the prefixes. Ten minutes per component, and it never lies about its width again.

## Build it with real blocks

Every section in this post is a component you can copy into your own codebase and own outright.

[Browse blocksBrowse blocks](/blocks)[Read more postsRead more posts](/blog)

## Ship the interface, keep the code.

New blocks, pieces and components every week. Install one with a command and it is yours to edit. No runtime dependency, no upgrade path to fight.

[Browse the libraryBrowse the library](/blocks)[See pricingSee pricing](/pricing)

### Library

* [Blocks](/blocks)
* [Pages](/pages)
* [Pieces](/pieces)
* [Components](/components)
* [Search](/search)

### Learn

* [Docs](/docs)
* [AI & MCP](/docs/mcp)
* [Blog](/blog)
* [Free tools](/tools)
* [What's new?](/changelog)
* [Website Builder](https://beste.co)

### More

* [Pricing](/pricing)
* [Referrals](/referrals)
* [License](/license)
* [GitHub](https://github.com/beste-co/beste-ui)

### Shadcn Blocks

* [Shadcn Hero Blocks](/blocks/hero)
* [Shadcn Feature Blocks](/blocks/feature)
* [Shadcn Pricing Blocks](/blocks/pricing)
* [Shadcn CTA Blocks](/blocks/cta)
* [Shadcn FAQ Blocks](/blocks/faq)
* [Shadcn About Blocks](/blocks/about)
* [Shadcn Stats Blocks](/blocks/stats)
* [Shadcn Footer Blocks](/blocks/footer)
* [Shadcn Navigation Blocks](/blocks/navigation)
* [Shadcn Auth Blocks](/blocks/auth)
* [Shadcn Ecommerce Blocks](/blocks/ecommerce)
* [Shadcn Portfolio Blocks](/blocks/portfolio)
* [Shadcn Showcase Blocks](/blocks/showcase)
* [Shadcn Careers Blocks](/blocks/careers)
* [Shadcn Onboarding Blocks](/blocks/onboarding)
* [Shadcn Coming Soon Blocks](/blocks/coming-soon)
* [Shadcn Post Blocks](/blocks/post)
* [Shadcn Legal Blocks](/blocks/legal)
* [Shadcn Workflow Blocks](/blocks/workflow)
* [Shadcn News Blocks](/blocks/news)

© 2026, [Beste](https://beste.co). All rights reserved.