{"id":400,"date":"2026-06-30T05:04:02","date_gmt":"2026-06-30T05:04:02","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/tailwind-v4-container-queries-the-media-queries-i-stopped-writing\/"},"modified":"2026-06-30T05:04:02","modified_gmt":"2026-06-30T05:04:02","slug":"tailwind-v4-container-queries-the-media-queries-i-stopped-writing","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/tailwind-v4-container-queries-the-media-queries-i-stopped-writing\/","title":{"rendered":"Tailwind v4 Container Queries: The Media Queries I Stopped Writing"},"content":{"rendered":"<p>I was rebuilding the sidebar on a side project last weekend, and I realized I&rsquo;d been writing the same useless media query for the last four years. You know the one: <code>lg:flex-row<\/code>, <code>md:grid-cols-2<\/code>, <code>xl:gap-6<\/code>. The component had no idea what page it was on. It just knew the viewport was wide. That&rsquo;s a different thing, and I&rsquo;d been pretending it wasn&rsquo;t for way too long.<\/p>\n<p>Then I noticed Tailwind v4 had moved container queries out of the plugin and into core. So I deleted about forty lines of breakpoint utilities, dropped a <code>@container<\/code> on the parent, and the component finally started behaving the way I&rsquo;d been wanting it to since 2021.<\/p>\n<p>This post is the short version of what changed for me, with the actual code I use now.<\/p>\n<h2 id=\"why-viewport-breakpoints-stopped-making-sense\">Why viewport breakpoints stopped making sense<\/h2>\n<p>A media query asks one question: how wide is the browser window. That&rsquo;s fine when your layout is a single column of full-bleed sections. It falls apart the second you reuse a component in a sidebar, a modal, or a two-up grid.<\/p>\n<p>I had a <code>&lt;UserCard \/&gt;<\/code> that looked great as a full-width hero. Drop it into a 320px sidebar and the avatar wrapped under the name. Drop it into a three-column dashboard and it had too much air. Every time, I&rsquo;d add another conditional class with a prop like <code>compact<\/code> or <code>variant=\"sidebar\"<\/code>. The component started looking like a switch statement wearing a costume.<\/p>\n<p>Container queries fix this by letting the component look at its parent, not the window. If the parent is 400px wide, the card lays out horizontally. If the parent is 200px wide, it stacks. The page can put it anywhere and it just works.<\/p>\n<h2 id=\"the-v4-syntax-that-finally-clicked-for-me\">The v4 syntax that finally clicked for me<\/h2>\n<p>In Tailwind v3 you had to install <code>@tailwindcss\/container-queries<\/code>, register it in your config, and remember which prefix did what. In v4 it&rsquo;s built in. You mark a container with <code>@container<\/code> and then prefix utilities with <code>@sm:<\/code>, <code>@md:<\/code>, <code>@lg:<\/code> for container-relative breakpoints. Or use arbitrary values like <code>@[400px]:flex-row<\/code> when you want a specific pixel.<\/p>\n<p>Here&rsquo;s the before. This is what <code>&lt;UserCard \/&gt;<\/code> looked like a month ago, using viewport breakpoints:<\/p>\n<pre><code class=\"language-jsx\">export function UserCard({ user }) {\n  return (\n    &lt;div className=&quot;flex flex-col gap-2 md:flex-row md:items-center md:gap-4 lg:gap-6 p-4&quot;&gt;\n      &lt;img\n        src={user.avatar}\n        className=&quot;w-12 h-12 md:w-16 md:h-16 rounded-full&quot;\n      \/&gt;\n      &lt;div className=&quot;flex flex-col&quot;&gt;\n        &lt;span className=&quot;font-semibold md:text-lg&quot;&gt;{user.name}&lt;\/span&gt;\n        &lt;span className=&quot;text-sm text-gray-500&quot;&gt;{user.role}&lt;\/span&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  );\n}\n<\/code><\/pre>\n<p>Looks reasonable. Breaks instantly the moment you put two of them side by side, because <code>md:<\/code> triggers on viewport width, not the card&rsquo;s own width.<\/p>\n<p>And here&rsquo;s the after, with container queries:<\/p>\n<pre><code class=\"language-jsx\">export function UserCard({ user }) {\n  return (\n    &lt;div className=&quot;@container&quot;&gt;\n      &lt;div className=&quot;flex flex-col gap-2 @sm:flex-row @sm:items-center @sm:gap-4 @md:gap-6 p-4&quot;&gt;\n        &lt;img\n          src={user.avatar}\n          className=&quot;w-12 h-12 @sm:w-16 @sm:h-16 rounded-full&quot;\n        \/&gt;\n        &lt;div className=&quot;flex flex-col&quot;&gt;\n          &lt;span className=&quot;font-semibold @sm:text-lg&quot;&gt;{user.name}&lt;\/span&gt;\n          &lt;span className=&quot;text-sm text-gray-500&quot;&gt;{user.role}&lt;\/span&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  );\n}\n<\/code><\/pre>\n<p>Now it lays out based on the wrapper&rsquo;s width. Drop it in a 240px sidebar, it stacks. Drop it in a 600px main panel, it goes horizontal. I didn&rsquo;t have to teach the parent anything.<\/p>\n<p>The official <a href=\"https:\/\/tailwindcss.com\/docs\/responsive-design#container-queries\" rel=\"nofollow noopener\" target=\"_blank\">Tailwind v4 container queries docs<\/a> cover every modifier, but <code>@sm<\/code>, <code>@md<\/code>, and <code>@lg<\/code> plus the arbitrary syntax handle about 90% of what I actually need.<\/p>\n<h2 id=\"where-i-actually-use-them-in-production\">Where I actually use them in production<\/h2>\n<p>Three places kept showing up in the last month of work.<\/p>\n<p><strong>Cards in unknown layouts.<\/strong> Anything I might drop into a Bento-style dashboard. The dashboard owner shouldn&rsquo;t have to know my component&rsquo;s preferred width.<\/p>\n<p><strong>Sidebar widgets.<\/strong> I have a notifications panel that lives in a 320px rail on desktop and goes full-width on mobile (where the rail disappears). Container queries let me write one set of classes that handles both contexts.<\/p>\n<p><strong>Modal contents.<\/strong> Modals get wider on tablets, narrower on phones, and sometimes a user resizes the window with the modal open. Viewport breakpoints don&rsquo;t catch a window resize from 1280px to 720px while a 600px modal is open. Container queries do.<\/p>\n<p>I still use viewport breakpoints for page-level chrome: nav bars, app shells, the marketing landing page where the layout actually does depend on the device. The rule of thumb I&rsquo;ve settled on is: viewport for layout-of-layouts, container for component internals.<\/p>\n<h2 id=\"named-containers-when-one-parent-isnt-enough\">Named containers, when one parent isn&rsquo;t enough<\/h2>\n<p>This is the bit I missed for the first week. By default <code>@sm:<\/code> looks at the nearest <code>@container<\/code> ancestor. If you nest containers, you might want a specific one. Tailwind v4 lets you name them with <code>@container\/sidebar<\/code> and then query against the name with <code>@sm\/sidebar:flex-row<\/code>.<\/p>\n<pre><code class=\"language-jsx\">&lt;aside className=&quot;@container\/sidebar w-80&quot;&gt;\n  &lt;div className=&quot;p-4&quot;&gt;\n    &lt;div className=&quot;@container\/widget&quot;&gt;\n      {\/* This responds to the widget container *\/}\n      &lt;div className=&quot;flex flex-col @sm\/widget:flex-row&quot;&gt;\n        ...\n      &lt;\/div&gt;\n\n      {\/* And this one explicitly responds to the sidebar *\/}\n      &lt;button className=&quot;hidden @sm\/sidebar:inline-block&quot;&gt;\n        Expand\n      &lt;\/button&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/aside&gt;\n<\/code><\/pre>\n<p>Not something I need every day, but the one time I did need it, I was glad it existed instead of having to compute parent widths in JavaScript like an animal.<\/p>\n<h2 id=\"a-few-sharp-edges-i-hit\">A few sharp edges I hit<\/h2>\n<p>A couple things tripped me up that I want you to skip.<\/p>\n<p>First, <code>@container<\/code> only works as a containment context if the element has <code>display<\/code> set to <code>block<\/code>, <code>inline-block<\/code>, or similar. A <code>display: contents<\/code> element won&rsquo;t work. Tailwind handles this for you if you just slap <code>@container<\/code> on a normal <code>&lt;div&gt;<\/code>, but if you&rsquo;re nesting it on something fancy, double-check the computed styles.<\/p>\n<p>Second, container queries don&rsquo;t compose with <code>dark:<\/code> the way I expected. You can chain them as <code>@sm:dark:bg-slate-900<\/code>, but the order matters and I got it wrong twice. The MDN page on <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_containment\/Container_queries\" rel=\"nofollow noopener\" target=\"_blank\">CSS container queries<\/a> is the cleanest reference I&rsquo;ve found for the underlying CSS once you&rsquo;re past the Tailwind wrapper.<\/p>\n<p>Third, browser support is fine in 2026 (Chrome, Safari, Firefox all shipped years ago), but if you have telemetry showing a long tail of users on iOS 15, container queries silently fall back to the mobile-first base styles. So write the mobile layout first and treat <code>@sm:<\/code> and up as progressive enhancement. Same discipline you&rsquo;d use for any CSS feature you can&rsquo;t polyfill.<\/p>\n<p>The Chrome team&rsquo;s <a href=\"https:\/\/web.dev\/blog\/cq-stable\" rel=\"nofollow noopener\" target=\"_blank\">web.dev intro to container queries<\/a> is worth a skim if you want the design history of how this got into the platform.<\/p>\n<h2 id=\"what-id-do-this-week-if-i-were-you\">What I&rsquo;d do this week if I were you<\/h2>\n<p>Pick one component you&rsquo;ve rewritten twice because it kept ending up in different layouts. Wrap its outermost div in <code>@container<\/code>. Replace every <code>md:<\/code> and <code>lg:<\/code> on that component&rsquo;s internals with <code>@sm:<\/code> and <code>@md:<\/code>. Drop it into two different parent widths in your Storybook (or just two siblings in a test page) and watch it behave.<\/p>\n<p>If you don&rsquo;t have a component like that, look at any card or list-item you&rsquo;ve ever rendered in both a sidebar and a main column. That&rsquo;s the one.<\/p>\n<p>One more thing if you&rsquo;re upgrading from v3. The <a href=\"https:\/\/abrarqasim.com\/blog\/tailwind-v4-oxide-engine-build-times-i-stopped-babysitting\" rel=\"noopener\">Tailwind v4 oxide engine post<\/a> I wrote a while back covers what changed in the build pipeline. The container query stuff plays nicely with the new engine, so you don&rsquo;t need to register anything extra.<\/p>\n<p>I&rsquo;ve been ripping container queries into older components for a couple of weeks now. About one in three rewrites lets me delete an entire prop. If you do enough of them, you&rsquo;ll start designing components that don&rsquo;t have layout opinions baked in, which is a nicer place to be working from. I cover this kind of thing in my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">front-end work<\/a> if you want to see it applied to bigger projects.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tailwind v4 ships container queries as first-class utilities. Here&#8217;s the @container pattern I now reach for, with before-and-after code from real components.<\/p>\n","protected":false},"author":2,"featured_media":399,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Tailwind v4 ships container queries as first-class utilities. Here's the @container pattern I now reach for, with before-and-after code from real components.","rank_math_focus_keyword":"tailwind css v4 features","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[433,35],"tags":[434,37,38,435,139,369,370],"class_list":["post-400","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tailwind-css","category-web-development","tag-container-queries-2","tag-css","tag-frontend","tag-responsive-design-2","tag-tailwind","tag-tailwind-css-2","tag-tailwind-v4-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/400","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=400"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/400\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/399"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=400"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=400"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=400"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}