{"id":330,"date":"2026-06-16T05:00:26","date_gmt":"2026-06-16T05:00:26","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/tailwind-css-v4-config-file-i-deleted\/"},"modified":"2026-06-16T05:00:26","modified_gmt":"2026-06-16T05:00:26","slug":"tailwind-css-v4-config-file-i-deleted","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/tailwind-css-v4-config-file-i-deleted\/","title":{"rendered":"Tailwind CSS v4: The Config File I Finally Deleted"},"content":{"rendered":"<p>Confession: I used to copy my <code>tailwind.config.js<\/code> from project to project like a lucky charm. I didn&rsquo;t fully understand every line in it. I knew which block set my colors and which block set my breakpoints, and the rest was cargo cult. If you&rsquo;d asked me what the <code>content<\/code> array globbing actually did under the hood, I&rsquo;d have changed the subject.<\/p>\n<p>Tailwind CSS v4 quietly took that file away from me, and after the initial &ldquo;wait, where did my config go&rdquo; panic, I like it a lot more than I expected. The short version: configuration moved out of a JavaScript file and into your CSS, the engine got dramatically faster, and a few things I used to fake (like container queries) are now first-class. Here&rsquo;s the before-and-after on the parts that changed how I actually work.<\/p>\n<h2 id=\"the-config-file-i-finally-deleted\">The config file I finally deleted<\/h2>\n<p>In v3, theme customization lived in JavaScript. You&rsquo;d extend the theme, restart the dev server, and hope you spelled <code>colors<\/code> right.<\/p>\n<pre><code class=\"language-js\">\/\/ tailwind.config.js (v3)\nmodule.exports = {\n  content: [&quot;.\/src\/**\/*.{html,js,jsx,ts,tsx}&quot;],\n  theme: {\n    extend: {\n      colors: {\n        brand: { DEFAULT: &quot;#2563eb&quot;, dark: &quot;#1e3a8a&quot; },\n      },\n      fontFamily: {\n        sans: [&quot;Inter&quot;, &quot;sans-serif&quot;],\n      },\n    },\n  },\n};\n<\/code><\/pre>\n<p>In v4, that moves into your CSS with the <code>@theme<\/code> directive. One import, no JavaScript config file for the common cases, and the values become real CSS variables you can read anywhere.<\/p>\n<pre><code class=\"language-css\">\/* app.css (v4) *\/\n@import &quot;tailwindcss&quot;;\n\n@theme {\n  --color-brand: #2563eb;\n  --color-brand-dark: #1e3a8a;\n  --font-sans: &quot;Inter&quot;, sans-serif;\n}\n<\/code><\/pre>\n<p>Now <code>bg-brand<\/code> works, and so does <code>var(--color-brand)<\/code> in any hand-written CSS, because the theme tokens are just variables. The first time I needed my brand color inside a tiny bit of custom CSS, I didn&rsquo;t have to import anything from a JS object. It was already there. The <a href=\"https:\/\/tailwindcss.com\/docs\/upgrade-guide\" rel=\"nofollow noopener\" target=\"_blank\">v4 upgrade guide<\/a> covers the migration path, including the automated tool that handles most of it for you.<\/p>\n<h2 id=\"automatic-content-detection-or-the-glob-i-stopped-maintaining\">Automatic content detection, or the glob I stopped maintaining<\/h2>\n<p>The <code>content<\/code> array was the source of my most embarrassing bugs. Add a new folder, forget to update the glob, and your classes silently don&rsquo;t generate in production. I lost an afternoon to that exact mistake more than once.<\/p>\n<p>v4 detects your template files automatically using heuristics based on your <code>.gitignore<\/code>, so the glob is gone for the common project layout. You can still point it at specific sources with <code>@source<\/code> when you need to, but I haven&rsquo;t needed to on a normal app. The <a href=\"https:\/\/tailwindcss.com\/docs\/detecting-classes-in-source-files\" rel=\"nofollow noopener\" target=\"_blank\">detecting classes docs<\/a> explain when the automatic behavior won&rsquo;t cover you, which is mostly when your markup lives somewhere unusual or in a dependency.<\/p>\n<p>That&rsquo;s one whole category of &ldquo;why isn&rsquo;t this style showing up&rdquo; debugging that just left my life.<\/p>\n<h2 id=\"container-queries-without-the-plugin-tax\">Container queries without the plugin tax<\/h2>\n<p>This is the one I&rsquo;m happiest about. In v3, responsive design keyed off the viewport. If you wanted a card that adapted to its container instead of the screen, you reached for the official container-queries plugin and installed yet another dependency.<\/p>\n<pre><code class=\"language-html\">&lt;!-- v3: needed @tailwindcss\/container-queries plugin --&gt;\n&lt;div class=&quot;@container&quot;&gt;\n  &lt;div class=&quot;@md:flex @md:gap-4&quot;&gt;...&lt;\/div&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n<p>v4 bakes container queries into core. Same <code>@container<\/code> and <code>@md:<\/code> syntax, no plugin to install.<\/p>\n<pre><code class=\"language-html\">&lt;!-- v4: built in --&gt;\n&lt;div class=&quot;@container&quot;&gt;\n  &lt;div class=&quot;grid @md:grid-cols-2 @lg:grid-cols-3 gap-4&quot;&gt;...&lt;\/div&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n<p>Why does this matter? Because real components don&rsquo;t know how wide the screen is. They know how wide their slot is. A card in a sidebar and the same card in a main column want different layouts, and viewport breakpoints can&rsquo;t tell them apart. I wrote up the messy way I used to fake this in <a href=\"https:\/\/abrarqasim.com\/blog\/tailwind-v4-container-queries-i-stopped-faking-responsiveness\" rel=\"noopener\">how I stopped faking responsiveness with container queries<\/a>, and v4 is the version where I finally stopped.<\/p>\n<p>The thing that makes container queries click is when you build a genuinely reusable component, like a product card you drop into a three-column grid, a two-column grid, and a narrow drawer, all in the same app. With viewport breakpoints you end up passing a <code>size<\/code> prop or writing three variants. With container queries the card just reads its own width and adapts, and you stop maintaining the variants entirely. That&rsquo;s the moment it goes from a neat trick to something I reach for by default.<\/p>\n<h2 id=\"the-speed-i-didnt-ask-for-but-wont-give-back\">The speed I didn&rsquo;t ask for but won&rsquo;t give back<\/h2>\n<p>v4 rewrote the engine, partly in Rust, and full builds got several times faster while incremental rebuilds dropped into the low single-digit milliseconds. The official <a href=\"https:\/\/tailwindcss.com\/blog\/tailwindcss-v4\" rel=\"nofollow noopener\" target=\"_blank\">v4.0 announcement<\/a> has the benchmarks, and your mileage depends on project size, but the practical effect for me was that the dev feedback loop stopped having a perceptible Tailwind step at all. Save, see the change. That&rsquo;s it.<\/p>\n<p>I&rsquo;ll be honest that speed wasn&rsquo;t on my list of complaints about v3. It was fine. But once the rebuild is instant you notice how often you were unconsciously waiting, and you don&rsquo;t want to go back.<\/p>\n<h2 id=\"new-things-in-the-utility-layer-worth-knowing\">New things in the utility layer worth knowing<\/h2>\n<p>A few smaller changes shifted how I write utilities day to day. The spacing scale is now dynamic, generated from a single <code>--spacing<\/code> base, so values like <code>p-13<\/code> or <code>mt-17<\/code> just work instead of failing because nobody happened to define them in the default scale. I used to add one-off spacing values to my config constantly. That whole habit is gone.<\/p>\n<p>Colors moved to the OKLCH color space, which sounds academic until you make a color slightly lighter and it stays the same hue instead of drifting muddy the way sRGB sometimes did. The default palette is more vivid on modern displays for the same reason. You don&rsquo;t have to do anything to get this; it&rsquo;s just how v4 ships.<\/p>\n<p>The other one I reach for is the <code>@utility<\/code> directive for custom utilities, which replaced the old <code>@layer utilities<\/code> plus <code>addUtilities<\/code> plugin dance. If I want a <code>content-auto<\/code> utility, I write it once in CSS and it participates in variants and hover states like a built-in.<\/p>\n<pre><code class=\"language-css\">\/* v4 custom utility *\/\n@utility content-auto {\n  content-visibility: auto;\n}\n<\/code><\/pre>\n<p>None of these are headline features. Together they&rsquo;re the reason I stopped opening a JavaScript config file reflexively every time I wanted something slightly custom. The custom value is usually a one-liner in CSS now, or it already exists because the scale is dynamic.<\/p>\n<h2 id=\"what-this-costs-you\">What this costs you<\/h2>\n<p>It&rsquo;s not free. v4 dropped support for older browsers; it targets Safari 16.4, Chrome 111, and Firefox 128 and up, because it leans on modern CSS features like cascade layers and <code>@property<\/code>. If you have to support older browsers, stay on v3, and that&rsquo;s a real constraint, not a footnote. The upgrade tool is good but not magic on heavily customized configs, so budget an afternoon for a large project rather than five minutes.<\/p>\n<p>A concrete thing to do this week: spin up a throwaway project with <code>npm install tailwindcss@latest<\/code>, move your color palette into an <code>@theme<\/code> block, and delete the JS config. Fifteen minutes will tell you whether the CSS-first approach clicks for you the way it did for me. If you want more of how I decide which framework changes are worth adopting, I keep notes on that across <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my projects<\/a>. I came into v4 grumpy about losing my config file and left it having deleted three other things I didn&rsquo;t miss either.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tailwind CSS v4 moves config into CSS, auto-detects content, and bakes in container queries. Before-and-after code on what changed and what it costs you.<\/p>\n","protected":false},"author":2,"featured_media":329,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Tailwind CSS v4 moves config into CSS, auto-detects content, and bakes in container queries. Before-and-after code on what changed and what it costs you.","rank_math_focus_keyword":"tailwind css v4","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[137,35],"tags":[37,38,369,370,172],"class_list":["post-330","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-css","category-web-development","tag-css","tag-frontend","tag-tailwind-css-2","tag-tailwind-v4-2","tag-web-development-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/330","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=330"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/330\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/329"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=330"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=330"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=330"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}