DEV Community

Craftly
Craftly

Posted on

Tailwind v4 Container Queries: A Practical Guide with Real Examples

Tailwind v4 Container Queries: A Practical Guide with Real Examples

Container queries landed as first-class citizens in Tailwind CSS v4. The short version: you can now style an element based on the size of its parent container, not just the viewport. That unlocks truly reusable components that adapt to wherever you drop them.

This guide shows the exact syntax, real-world patterns we use in every Craftly template, and the three gotchas that aren't in the docs.

What problem container queries solve

Before container queries, responsive design was anchored to viewport width. A card component on a full-width mobile layout and a card component tucked into a 300px sidebar on desktop would both get the same media-query breakpoints. The sidebar card looked broken.

Container queries fix this by letting the card respond to its own available width instead. Move it from full-width to sidebar and it adapts, same component.

The basic pattern in Tailwind v4

Three steps:

Step 1: Mark the container

Put the @container class on whatever parent should be the query target:

<div className="@container">
  <Card />
</div>
Enter fullscreen mode Exit fullscreen mode

Step 2: Use container variants on children

Inside Card, use @sm:, @md:, @lg: etc. as variant prefixes — they key off the container, not the viewport:

<div className="p-4 @sm:p-6 @lg:p-8">
  <h3 className="text-base @md:text-lg @lg:text-xl">Card title</h3>
</div>
Enter fullscreen mode Exit fullscreen mode

Now that card responds to its own parent's width. Drop it in a full-width grid cell, it renders at the @lg size. Drop it in a narrow sidebar, it stays at the base size.

Step 3: Container sizes default

The default breakpoints are:

Variant Container width
@xs 20rem (320px)
@sm 24rem (384px)
@md 28rem (448px)
@lg 32rem (512px)
@xl 36rem (576px)
@2xl 42rem (672px)

You can customize these in your @theme inline block if the defaults don't match your layout.

A real example: a reusable product card

Here's the pattern we use in SaaSify and the Portfolio template. Same component, three contexts:

function ProductCard({ product }) {
  return (
    <div className="@container">
      <article className="rounded-2xl border bg-card p-4 @sm:p-6 @lg:flex @lg:items-center @lg:gap-8">
        <img
          src={product.image}
          className="aspect-square w-full rounded-lg object-cover
                     @sm:aspect-[4/3]
                     @lg:aspect-square @lg:w-48 @lg:flex-shrink-0"
        />
        <div className="mt-4 @lg:mt-0">
          <h3 className="text-lg font-semibold @md:text-xl @lg:text-2xl">
            {product.name}
          </h3>
          <p className="mt-2 text-sm text-muted-foreground @md:text-base">
            {product.description}
          </p>
          <div className="mt-4 @lg:mt-6">
            <span className="text-2xl font-bold">${product.price}</span>
          </div>
        </div>
      </article>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Drop this into any grid and it adapts:

// Full-width stacking mobile
<div className="space-y-4">
  <ProductCard product={p} />
</div>

// 3-column grid on desktop — each cell renders at @md
<div className="grid grid-cols-3 gap-6">
  <ProductCard product={p} />
</div>

// Narrow sidebar — stays at base size
<aside className="w-64">
  <ProductCard product={p} />
</aside>
Enter fullscreen mode Exit fullscreen mode

No media-query fiddling. No prop threading a size variant. Just the card, looking right everywhere.

Gotcha 1: The @container parent needs explicit sizing

This trips everyone. For container queries to work, the @container parent needs to have a width that the browser can measure. If the parent is inside a flex column with no explicit width, or inside a grid cell with min-content, container queries won't fire until a parent establishes sizing.

Usually this "just works" because most layouts have some grid or flex sizing. But when the parent has no size, add w-full or put it in a container with known width:

// Won't work inside a flex parent without sized children
<div className="flex">
  <div className="@container">
    <Card />  {/* @sm: never fires */}
  </div>
</div>

// Works — explicit width
<div className="flex">
  <div className="@container w-full">
    <Card />
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: Container vs viewport variants are distinct

The @md: variant (container) and the md: variant (viewport) are different. Don't mix them thinking one falls back to the other.

// These are different
<div className="text-base md:text-lg @md:text-xl">
  {/* md:text-lg fires at viewport 768px+ */}
  {/* @md:text-xl fires at container 28rem+ */}
</div>
Enter fullscreen mode Exit fullscreen mode

Pick one per component, and usually @ (container) wins for reusability.

Gotcha 3: Named containers for multi-level queries

If a component nests inside another @container, you might want children to query the outer container, not the nearest one. Tailwind v4 supports named containers for this:

<div className="@container/outer">
  <div className="@container/inner">
    <p className="@lg/outer:text-xl">
      {/* Queries the outer, ignoring the inner */}
    </p>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Most projects never need this — if you do, name your containers after the semantic role (@container/card, @container/sidebar) rather than position.

Performance notes

Container queries are implemented in the browser with container-type: inline-size (or size for two-axis queries). The reflow cost is real but small — Chrome's implementation handles thousands of container-query elements without frame drops.

Tailwind v4 only emits the container-type on elements you explicitly mark with @container, so you don't pay the cost globally.

Browser support

As of 2026:

  • Chrome/Edge 105+ ✅
  • Safari 16+ ✅
  • Firefox 110+ ✅

Which is to say: everyone who updated Chrome in the last two years has them. The @container feature is firmly in the "can use without guilt" category.

When not to use container queries

For layout decisions that depend on user viewport (hamburger menu vs full nav), you still want viewport breakpoints. Container queries are for component-level responsiveness, not app-level layout.

Rule of thumb: if the component is reusable across multiple layout slots, use @container. If the decision is global (site header, main content column), use md:/lg: viewport breakpoints.

Migrating existing components

If you already have responsive components using viewport breakpoints, you don't have to migrate everything. A sensible refactor:

  1. Identify the 3-5 components you reuse in multiple layout contexts
  2. Wrap each in @container
  3. Replace sm:@sm:, md:@md: inside those components
  4. Test by dropping them into different layout slots

The rest of your app can stay viewport-based.

Wrapping up

Container queries in Tailwind v4 let one component serve full-width, grid-cell, and sidebar contexts without duplication or prop-drilled size variants. The three gotchas — sized parents, container vs viewport distinction, and named containers — account for 90% of "why isn't this working" moments. Once you internalize them, you'll wonder how you shipped responsive components before.

Every Craftly template uses @container for its primary card components. If you want to see real-world patterns, browse the catalog at getcraftly.dev.

Top comments (0)