[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-replaced-tailwind-native-css":3},"\u003Cp>I've been writing Tailwind since v1. Five years, roughly, long enough that \u003Ccode>flex items-center justify-between\u003C\u002Fcode> used to come out of my fingers before I'd finished thinking the sentence. So when I removed it from a production app last month, I expected to hate the result.\u003C\u002Fp>\n\n\u003Cp>I didn't. That's the part I'm still getting used to.\u003C\u002Fp>\n\n\u003Cp>The app is a small marketing site plus a dashboard, around thirty screens. Tailwind wasn't the problem there. Nothing was slow, nothing was broken, nobody was complaining about the stack. I pulled it out anyway, mostly because I was tired of the config drift and the PostCSS chain, and honestly? A little tired of \u003Ccode>lg:flex-row\u003C\u002Fcode> and \u003Ccode>dark:bg-slate-900\u003C\u002Fcode> repeating a design system I'd already written once in CSS custom properties.\u003C\u002Fp>\n\n\u003Cp>Here's the report from the other side.\u003C\u002Fp>\n\n\u003Ch2>The first hour is the hardest\u003C\u002Fh2>\n\n\u003Cp>The muscle memory is real. My first component after the migration started with \u003Ccode>mb-4\u003C\u002Fcode> and \u003Ccode>p-6\u003C\u002Fcode> written by habit, attached to class names that no longer meant anything. It took about an hour of frustration before I stopped reaching for the old reflexes.\u003C\u002Fp>\n\n\u003Cp>I kept the mental structure Tailwind taught me, though. Layers, a small utility set, tokens. That part transfers fine. \u003Ccode>@layer\u003C\u002Fcode> gives you the same ordering control without the framework:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-css\">@layer reset, tokens, base, components, utilities;\n\n@layer tokens {\n  :root {\n    --space-1: 4px;\n    --space-2: 8px;\n    --space-3: 12px;\n    --space-4: 16px;\n    --radius: 8px;\n    --brand: oklch(55% 0.2 255);\n  }\n}\n\n@layer components {\n  .btn {\n    padding: var(--space-2) var(--space-4);\n    border-radius: var(--radius);\n    background: var(--brand);\n    color: white;\n\n    &:hover { filter: brightness(1.1); }\n\n    &[data-variant=\"ghost\"] {\n      background: transparent;\n      color: var(--brand);\n    }\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>That nesting is native CSS now. No Sass, no build step, no \u003Ccode>@apply\u003C\u002Fcode>. The \u003Ccode>&amp;\u003C\u002Fcode> behaves the way you already expect it to.\u003C\u002Fp>\n\n\u003Ch2>What actually carried the migration\u003C\u002Fh2>\n\n\u003Cp>Three features did most of the work.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Custom properties for everything.\u003C\u002Fstrong> Dark mode used to be \u003Ccode>dark:\u003C\u002Fcode> prefixes scattered across every template file. Now it's one media query that swaps the tokens:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-css\">@media (prefers-color-scheme: dark) {\n  :root {\n    --bg: oklch(20% 0.01 250);\n    --text: oklch(92% 0.01 250);\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>And \u003Ccode>color-mix()\u003C\u002Fcode> replaced my entire hand-maintained tint and shade palette. \u003Ccode>color-mix(in oklch, var(--brand) 12%, white)\u003C\u002Fcode> is a hover state, not a new swatch in the config file.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>\u003Ccode>:has()\u003C\u002Fcode> deleted a category of JS.\u003C\u002Fstrong> The dashboard has form fields that need to turn red when invalid. That used to be \u003Ccode>aria-invalid\u003C\u002Fcode> plus a class toggle plus a watcher. Now it's one rule:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-css\">.field:has(input:user-invalid) {\n  border-color: oklch(55% 0.2 25);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>It re-evaluates live as the user types. I deleted the validation styling logic entirely.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Container queries ended a lie I'd been telling myself.\u003C\u002Fstrong> The dashboard has widgets that live in both a narrow sidebar and a wide main column. Same component, two layouts. With Tailwind I handled that with responsive prefixes tuned to the page's breakpoints, which is a roundabout way of saying \"I guessed.\" Container queries respond to the space the widget actually gets:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-css\">.widget { container-type: inline-size; }\n\n@container (min-width: 500px) {\n  .widget__body { grid-template-columns: 1fr 2fr; }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch2>What I genuinely miss\u003C\u002Fh2>\n\n\u003Cp>The migration wasn't a clean win, and I'd be lying if I said it was.\u003C\u002Fp>\n\n\u003Cp>The \u003Ccode>hover:\u003C\u002Fcode>\u002F\u003Ccode>focus:\u003C\u002Fcode>\u002F\u003Ccode>dark:\u003C\u002Fcode> modifier syntax is just more compact than writing the selectors by hand. \u003Ccode>.btn:hover\u003C\u002Fcode> and \u003Ccode>.btn:focus-visible\u003C\u002Fcode> next to each other is more lines. Not more complex. More bytes. Fine for thirty screens. I'd think hard before doing this on a three-hundred-screen codebase with a design system team.\u003C\u002Fp>\n\n\u003Cp>You also have to know CSS. That's the real price of admission. Tailwind enforced consistency. My \u003Ccode>p-4\u003C\u002Fcode> was your \u003Ccode>p-4\u003C\u002Fcode> across the whole company. Plain CSS hands that discipline back to the humans. Custom property names become your API, and bad names surface at the worst possible moment.\u003C\u002Fp>\n\n\u003Cp>And file size needs babysitting. Tailwind purges unused classes for you. Hand-written CSS means every rule is someone's responsibility. I shipped a stale stylesheet to production and only noticed because the dashboard felt sluggish. A \u003Ccode>stylelint\u003C\u002Fcode> pass and a size check in CI fixed it, but that's tooling I didn't need before.\u003C\u002Fp>\n\n\u003Ch2>Would I do it again\u003C\u002Fh2>\n\n\u003Cp>The migration is three weeks old now. The CSS file is smaller than the generated output was, the markup is readable again, and I deleted three config files. For a small app, it's a straight win.\u003C\u002Fp>\n\n\u003Cp>For a big team? Probably not. Tailwind earns its keep when ten people need to share a design language without debating it in every PR. But for solo work and small teams, the platform caught up. That's not a hot take anymore, it's just where we are.\u003C\u002Fp>\n\n\u003Cp>I saved the layer scaffold and the container-query widget pattern in \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> so the next project starts from the working version instead of a blank file. Starting with a clean layer structure is the difference between a weekend project and a two-week regret.\u003C\u002Fp>\n",1787652455097]