{"id":147,"date":"2026-04-24T13:02:55","date_gmt":"2026-04-24T13:02:55","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/tailwind-container-queries-i-finally-get-it\/"},"modified":"2026-04-24T13:02:55","modified_gmt":"2026-04-24T13:02:55","slug":"tailwind-container-queries-i-finally-get-it","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/tailwind-container-queries-i-finally-get-it\/","title":{"rendered":"Tailwind Container Queries: The One Feature I Wish I&#8217;d Used Sooner"},"content":{"rendered":"<p>Confession: I spent two years telling myself container queries were a &ldquo;nice to have&rdquo; and kept shipping media-query hacks instead. Then last month a sidebar-width card component finally forced my hand, and within an afternoon I was quietly rewriting four older components to use them. If you&rsquo;ve also been squinting at the docs and thinking &ldquo;yeah I&rsquo;ll get to it,&rdquo; this post is for you.<\/p>\n<p>I&rsquo;m going to walk through the exact before\/after code, when I reach for <code>@container<\/code> vs a media query, and the one footgun that tripped me up for a solid hour. No theory lecture. Just the stuff I actually needed.<\/p>\n<h2 id=\"what-container-queries-actually-solve\">What container queries actually solve<\/h2>\n<p>The problem: a card component has no idea how wide its parent is. You might drop the same card into a full-width hero slot on one page and a narrow sidebar on another. Media queries only see the viewport, so the card&rsquo;s layout responds to the wrong thing.<\/p>\n<p>Media queries say &ldquo;when the screen is 768px wide.&rdquo; Container queries say &ldquo;when <em>this container<\/em> is 400px wide.&rdquo; That&rsquo;s the whole idea. The browser-level feature landed in stable Chrome, Safari, and Firefox back in 2023, and coverage is now sitting above 93% globally if you check <a href=\"https:\/\/caniuse.com\/css-container-queries\" rel=\"nofollow noopener\" target=\"_blank\">caniuse for container queries<\/a>. I stopped worrying about the fallback question somewhere last year.<\/p>\n<p>Tailwind shipped first-class support for them in v4 as a core plugin. In v3 you needed <code>@tailwindcss\/container-queries<\/code> installed separately; in v4 it&rsquo;s baked in, so you get the <code>@container<\/code> directive and the <code>@sm:<\/code>, <code>@md:<\/code>, <code>@lg:<\/code> variants with no extra config. The <a href=\"https:\/\/tailwindcss.com\/docs\/responsive-design#container-queries\" rel=\"nofollow noopener\" target=\"_blank\">official Tailwind docs on container queries<\/a> are actually pretty good now, which wasn&rsquo;t always true.<\/p>\n<h2 id=\"the-before-code-im-mildly-embarrassed-about\">The before code I&rsquo;m mildly embarrassed about<\/h2>\n<p>Here&rsquo;s the card I had in three places across a dashboard, with viewport-based responsive classes that looked fine on the homepage and broke in a sidebar:<\/p>\n<pre><code class=\"language-jsx\">\/\/ before: card that lies to itself about its width\nexport function StatCard({ label, value, trend }) {\n  return (\n    &lt;div className=&quot;rounded-2xl border p-4 md:p-6 lg:p-8&quot;&gt;\n      &lt;div className=&quot;flex flex-col md:flex-row md:items-center md:justify-between gap-2&quot;&gt;\n        &lt;p className=&quot;text-sm text-gray-500&quot;&gt;{label}&lt;\/p&gt;\n        &lt;p className=&quot;text-2xl md:text-3xl lg:text-4xl font-semibold&quot;&gt;{value}&lt;\/p&gt;\n      &lt;\/div&gt;\n      &lt;p className=&quot;mt-2 text-xs text-gray-400&quot;&gt;{trend}&lt;\/p&gt;\n    &lt;\/div&gt;\n  );\n}\n<\/code><\/pre>\n<p>Drop that card into a 280px sidebar at desktop width and you get the <code>md:flex-row<\/code> layout squashed into a tiny column, with a 3xl font sitting above a ten-character value. The card doesn&rsquo;t know it&rsquo;s small. The viewport is wide, so Tailwind happily applies the big-screen classes.<\/p>\n<p>The hack I used to get around this was passing in a <code>size=\"sm\"<\/code> prop and branching on it. That scales badly. Three callsites turn into ten, and now the card component has to know about every layout it might land in.<\/p>\n<h2 id=\"the-after-code-with-container-doing-the-work\">The after code, with @container doing the work<\/h2>\n<p>Same component, rewritten to respond to its own width instead of the viewport:<\/p>\n<pre><code class=\"language-jsx\">\/\/ after: card that reads its own container\nexport function StatCard({ label, value, trend }) {\n  return (\n    &lt;div className=&quot;@container rounded-2xl border p-4 @md:p-6 @lg:p-8&quot;&gt;\n      &lt;div className=&quot;flex flex-col @md:flex-row @md:items-center @md:justify-between gap-2&quot;&gt;\n        &lt;p className=&quot;text-sm text-gray-500&quot;&gt;{label}&lt;\/p&gt;\n        &lt;p className=&quot;text-2xl @md:text-3xl @lg:text-4xl font-semibold&quot;&gt;{value}&lt;\/p&gt;\n      &lt;\/div&gt;\n      &lt;p className=&quot;mt-2 text-xs text-gray-400&quot;&gt;{trend}&lt;\/p&gt;\n    &lt;\/div&gt;\n  );\n}\n<\/code><\/pre>\n<p>Two changes. I added <code>@container<\/code> to the outer div, which tells the browser &ldquo;measure this element and use its width for any <code>@<\/code> variant children.&rdquo; Then I swapped every <code>md:<\/code> for <code>@md:<\/code>. That&rsquo;s it. Now when the card lands in the sidebar, the flex-column layout stays. When it lands in a hero slot, the flex-row layout kicks in. The card stopped lying to itself.<\/p>\n<p>The <code>@md<\/code> breakpoint in Tailwind v4 is 448px of container width by default, not viewport. <code>@sm<\/code> is 384px, <code>@lg<\/code> is 512px, <code>@xl<\/code> is 576px. You can find the full table in the <a href=\"https:\/\/tailwindcss.com\/docs\/responsive-design#container-queries\" rel=\"nofollow noopener\" target=\"_blank\">Tailwind v4 container query reference<\/a>, and you can override the scale if your design system uses different breakpoints.<\/p>\n<h2 id=\"the-footgun-that-cost-me-an-hour\">The footgun that cost me an hour<\/h2>\n<p>Here&rsquo;s the thing nobody tells you: the element with <code>@container<\/code> on it <strong>cannot<\/strong> also use its own <code>@md:<\/code> classes. Container queries don&rsquo;t self-reference. So this looks reasonable but does absolutely nothing:<\/p>\n<pre><code class=\"language-html\">&lt;div class=&quot;@container @md:flex-row flex-col&quot;&gt;\n  ...\n&lt;\/div&gt;\n<\/code><\/pre>\n<p>The <code>@md:flex-row<\/code> won&rsquo;t fire even when the div is wide, because the div is its own container, and the container&rsquo;s width at query time is measured <em>from the child&rsquo;s perspective<\/em>. The div isn&rsquo;t a child of itself.<\/p>\n<p>Fix: wrap it. Put <code>@container<\/code> on the outer element, and put the <code>@md:<\/code> classes on the inner content. I lost a solid hour to this the first time, staring at the DevTools and wondering why my selector was never matching.<\/p>\n<h2 id=\"when-i-reach-for-media-queries-instead\">When I reach for media queries instead<\/h2>\n<p>Container queries are not a full replacement. I still use viewport media queries for:<\/p>\n<ul>\n<li><strong>Page-level layout.<\/strong> If the whole grid goes from 3 columns to 1 when the phone is held vertically, that&rsquo;s a viewport thing. The grid container&rsquo;s width <em>is<\/em> the viewport&rsquo;s width, so there&rsquo;s nothing to measure separately.<\/li>\n<li><strong>Typography scale.<\/strong> Body copy should read well on a phone vs a laptop vs a 4K monitor. That&rsquo;s a viewport concern, not a container concern. I set <code>text-base lg:text-lg<\/code> on the <code>&lt;body&gt;<\/code> and move on.<\/li>\n<li><strong>Navigation chrome.<\/strong> A hamburger menu that collapses on mobile is keyed to the viewport, not some container.<\/li>\n<\/ul>\n<p>Container queries earn their keep inside reusable components: cards, list items, form rows, media embeds, anything that gets reused at different sizes. The rule I use now is &ldquo;if I might drop this thing into more than one width of parent, it gets <code>@container<\/code>.&rdquo;<\/p>\n<p>I wrote a longer piece on how v4&rsquo;s core set of changes fit together in my <a href=\"https:\/\/abrarqasim.com\/blog\/tailwind-css-v4-migration-guide-what-actually-changed\/\" rel=\"noopener\">Tailwind CSS v4 migration guide<\/a> if you want the broader context on what&rsquo;s new.<\/p>\n<h2 id=\"a-real-example-from-a-client-dashboard\">A real example from a client dashboard<\/h2>\n<p>On a dashboard I shipped for a client last month (one of the projects I list on my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">portfolio page<\/a>), there was a &ldquo;recent activity&rdquo; feed that appeared both as a full-width panel on the overview screen and as a skinny right-rail on every detail screen. Same component, two completely different widths.<\/p>\n<p>Before container queries: I passed a <code>variant=\"compact\"<\/code> prop and conditionally rendered a stripped-down version. Two code paths, two sets of tests, two sets of screenshots in the design system doc.<\/p>\n<p>After: one component. It has <code>@container<\/code> on the wrapper and uses <code>@sm:<\/code> to decide whether to show avatars, <code>@md:<\/code> to decide whether to show timestamps inline or stacked, and <code>@lg:<\/code> to decide whether to show a right-rail filter. Both callsites just drop the component in. The skinny panel hides avatars automatically because its container is under 384px wide. The wide panel shows everything. I deleted roughly forty lines of prop-plumbing code.<\/p>\n<p>That&rsquo;s the moment it clicked for me. Container queries aren&rsquo;t &ldquo;responsive design 2.0.&rdquo; They&rsquo;re &ldquo;prop-drilling for layout, but the browser does it for you.&rdquo;<\/p>\n<h2 id=\"the-named-containers-thing-skip-if-youre-just-starting\">The named containers thing (skip if you&rsquo;re just starting)<\/h2>\n<p>One level deeper: you can name a container and target it by name from anywhere inside. Useful when you have nested containers and need to read a specific ancestor.<\/p>\n<pre><code class=\"language-jsx\">&lt;section className=&quot;@container\/sidebar&quot;&gt;\n  &lt;div className=&quot;@container\/card rounded-xl&quot;&gt;\n    &lt;p className=&quot;@md\/sidebar:text-lg&quot;&gt;I respond to the sidebar, not the card.&lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/section&gt;\n<\/code><\/pre>\n<p>The <code>\/sidebar<\/code> suffix names the container. Then <code>@md\/sidebar:<\/code> specifically queries the container named <code>sidebar<\/code>. I use this maybe 5% of the time, usually for widget grids where a cell&rsquo;s layout depends on the whole grid&rsquo;s width, not its own width.<\/p>\n<p>Tailwind&rsquo;s <a href=\"https:\/\/tailwindcss.com\/docs\/responsive-design#container-query-range-types\" rel=\"nofollow noopener\" target=\"_blank\">container query syntax reference<\/a> covers the whole set if you want to go deeper, including range queries and <code>@max<\/code> variants.<\/p>\n<h2 id=\"one-thing-you-can-do-this-week\">One thing you can do this week<\/h2>\n<p>Open your codebase and grep for components that accept a <code>size<\/code> or <code>variant<\/code> prop just to switch layout. Pick one. Rewrite it to use <code>@container<\/code> and the <code>@md:<\/code> variants instead. Delete the prop.<\/p>\n<p>That&rsquo;s it. You&rsquo;ll know within fifteen minutes whether this pattern is going to save you work. For me it was obvious by the second component I tried. The prop-drilling you stop doing pays for the five minutes of squinting at the docs, and after the third component the muscle memory kicks in.<\/p>\n<p>I still get a small thrill every time I drop a component into a new layout and it just looks right. That used to be a feeling CSS didn&rsquo;t give me.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I ignored Tailwind container queries for two years and it cost me. Here&#8217;s the before\/after of a card component I rebuilt, and when @container beats a media query.<\/p>\n","protected":false},"author":2,"featured_media":146,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I ignored Tailwind container queries for two years and it cost me. Here's the before\/after of a card component I rebuilt, and when @container beats a media query.","rank_math_focus_keyword":"tailwind container queries","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[137,138],"tags":[141,37,38,140,139],"class_list":["post-147","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-css","category-frontend","tag-container-queries","tag-css","tag-frontend","tag-responsive-design","tag-tailwind"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/147","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=147"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/147\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/146"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=147"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=147"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=147"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}