Skip to content

✍️ 필사 모드: Tailwind CSS 4 Deep Dive — Oxide Engine, Vite-First Architecture, CSS-First Config, and a Real v3 Migration Story for 2026

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Prologue — When did you last open tailwind.config.js?

On January 22, 2025, Tailwind CSS v4.0 shipped as GA. The first line of the release post was blunt: "Tailwind CSS 4.0 is a new framework, written from scratch." Not a patch note, a re-launch.

A year later, that claim is not overstated.

  • The engine was rewritten in Rust (Oxide). Full builds got about 3.5x faster, incremental builds got over 100x faster.
  • tailwind.config.js is gone. Config now lives inside CSS, in a @theme block.
  • The content: [] array is gone. The engine auto-scans the project.
  • A PostCSS plugin is no longer required. The Vite plugin is the primary integration, with the CLI as the fallback.
  • The base/components/utilities split now maps to real CSS cascade layers.
  • color-mix(), OKLCH, P3 color, container queries, and @starting-style are all in the default toolchain.

This post unpacks v4 with one year of production experience behind it. Not just a migration checklist — the goal is to explain why it was built this way, what got better and what got lost, and when to upgrade versus when to hold.


Chapter 1 · The Oxide Engine — A Rust-Powered Core

1.1 What actually changed

Tailwind through v3 was a Node.js project. JIT mode made it fast, but the core was still JavaScript running on top of PostCSS. The Oxide engine in v4 changes three things at once.

  1. A Rust-based core. The parser, source scanner, and CSS generator are all compiled Rust. Node bootstrap and V8 JIT warm-up costs disappear.
  2. Lightning CSS is integrated. Vendor prefixing, CSS down-leveling, and minification happen inside the same Rust pipeline. Previously you ran Autoprefixer and cssnano as separate PostCSS plugins.
  3. A parallel scanner. The Rust-side scanner walks the project tree on multiple threads. The win is largest in big monorepos.

1.2 Real benchmarks

The Tailwind team's published benchmarks.

Scenariov3.4v4.0Speedup
Catalyst full build378ms100msabout 3.8x
Catalyst incremental (new CSS)44ms5msabout 8.8x
Catalyst incremental (class only)35ms192usabout 182x
Tailwind.com full build960ms105msabout 9.1x
Tailwind.com incremental (no CSS)21ms192usabout 109x

On our internal design system (about 3,800 components, 220 pages) the numbers tracked closely.

  • Full build: 4.1s to 0.92s, about 4.5x
  • Incremental build: avg 28ms to 1.1ms, about 25x

The biggest perceptual change is that incremental builds drop from milliseconds to microseconds. HMR finally feels truly instant.

1.3 Why Rust at all

Tailwind was already fast in v3. So why rewrite the entire core in a new language?

  • Cut JS bootstrap overhead. On small builds, Node startup plus V8 warm-up took longer than the actual build.
  • Natural parallelism. Rust with rayon parallelizes a directory walk almost for free. Doing the same with worker_threads in Node costs you serialization.
  • Lightning CSS integration. Lightning CSS is itself Rust. Living in the same memory model is the sensible choice.
  • Native binary distribution. Installing the package pulls in a prebuilt binary per platform. It works the same on Bun, Deno, or Node.

Chapter 2 · Vite-First — PostCSS Is No Longer the Default

2.1 The new integration priority

In v3, the PostCSS plugin was the standard integration. Vite, Webpack, Next.js, and CRA all routed through PostCSS. v4 inverts this priority list.

  1. Vite plugin@tailwindcss/vite. Primary.
  2. CLI@tailwindcss/cli. Secondary.
  3. PostCSS plugin@tailwindcss/postcss. Kept for compatibility.

The reason for moving Vite to the top is simple. Vite handles CSS lazily in dev. Routing through PostCSS means paying that cost on every change. The dedicated Vite plugin calls Lightning CSS directly and finishes CSS transformation inside the dev server.

2.2 Install — v3 vs v4

Standard v3 Vite setup.

pnpm add -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p

The v4 Vite setup.

pnpm add tailwindcss @tailwindcss/vite

No postcss.config.js, no tailwind.config.js. You add the plugin to vite.config.ts.

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [tailwindcss()],
})

A single import in your entry CSS.

@import "tailwindcss";

Done. The familiar v3 three-liner @tailwind base; @tailwind components; @tailwind utilities; collapses to that one line in v4.

2.3 Next.js integration

Next.js 14, 15, and 16 still use a PostCSS-based pipeline (including Turbopack), so the PostCSS plugin is the path.

pnpm add tailwindcss @tailwindcss/postcss
// postcss.config.mjs
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

Important detail. v4 handles Autoprefixer automatically. Stop adding autoprefixer to your PostCSS chain — it duplicates work and can cause warnings.

2.4 Legacy bundlers — webpack, esbuild, Parcel

These still work through the PostCSS path. The new architecture really shines with Vite. If you are starting a new project, take Vite.


Chapter 3 · CSS-First Config — @theme Replaces tailwind.config.js

3.1 A real paradigm shift

Through v3, config was a JS object.

// tailwind.config.js (v3)
module.exports = {
  content: ['./src/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a8a',
        },
      },
      fontFamily: {
        display: ['Inter', 'system-ui', 'sans-serif'],
      },
      spacing: {
        '128': '32rem',
      },
    },
  },
  plugins: [require('@tailwindcss/typography')],
}

v4 expresses the same config inside CSS.

/* app.css (v4) */
@import "tailwindcss";

@theme {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a8a;

  --font-display: "Inter", "system-ui", sans-serif;

  --spacing-128: 32rem;
}

@plugin "@tailwindcss/typography";

The key change is not just "the file moved location."

  • Tailwind tokens are now CSS custom properties. --color-brand-500 is also var(--color-brand-500). Your JS at runtime can read it.
  • Utilities are generated automatically. Declare --color-brand-500 and you get bg-brand-500, text-brand-500, border-brand-500 for free.
  • Dynamic theming becomes trivial. Dark mode and theme swaps are just overwriting CSS variables.

3.2 The token namespaces

Inside @theme, the prefix matters. The core namespaces.

NamespaceExampleUtilities generated
--color-*--color-brand-500bg-brand-500, text-brand-500, ...
--font-*--font-displayfont-display
--text-*--text-basetext-base (font-size)
--spacing-*--spacing-128p-128, mx-128, ...
--breakpoint-*--breakpoint-3xlmedia queries like 3xl:flex
--radius-*--radius-xlrounded-xl
--shadow-*--shadow-glowshadow-glow
--ease-*--ease-out-quartease-out-quart
--animate-*--animate-shimmeranimate-shimmer

This naming is not a convention — it is the interface the compiler reads. Get the prefix wrong and no utility is generated.

3.3 A bonus — single source of truth for design tokens

A pattern from v3: a design system package exports tokens.json, and a build step turns it into tailwind.config.js. v4 lets you delete that build step.

@import "tailwindcss";

/* design-tokens.css — auto-generated from Figma or Style Dictionary */
@import "@acme/design-tokens/dist/css";

@theme {
  /* Surface design tokens as Tailwind tokens directly */
  --color-primary-500: var(--ds-color-primary-500);
  --color-primary-700: var(--ds-color-primary-700);
}

The "design tokens to Tailwind" converter goes from N lines to zero.


Chapter 4 · Death of the content Array — Automatic Source Detection

4.1 The v3 pain

The single most common v3 debug question was "why is this class not generated?" The answer was almost always the content array.

// Typical v3 failure
content: [
  './src/**/*.{ts,tsx}',
  // ❌ a UI package under packages/ui is missing
],

In monorepos this gets worse — you list every UI package, every feature package, every app, and every time a new package shows up you edit config.

4.2 v4 auto-detection

v4 removes the content array. Sources are detected with the following rules.

  1. Start from the directory that contains your CSS entry point.
  2. Skip anything matched by .gitignore or .tailwindignore.
  3. Skip binaries, images, bundles, and node_modules.
  4. Include packages from node_modules only when they explicitly export their CSS classes (think flowbite, @shadcn/ui).

The Rust scanner runs in parallel, so this is fast even on large projects.

4.3 When you need manual control

If you do not like the auto-detection, or you must force-include a third-party package, you declare it inside CSS.

@import "tailwindcss";

/* Force this package into the scan */
@source "../node_modules/@acme/legacy-components";

/* Exclude a directory */
@source not "../docs";

The @source directive is new in v4 and takes glob patterns. It is not less expressive than the v3 content array — just relocated.


Chapter 5 · Native Modern CSS

The biggest philosophical shift from v3 to v4 is "let CSS do what CSS is good at."

5.1 Container queries

In v3 you installed @tailwindcss/container-queries separately. v4 absorbs it into the core.

<div class="@container">
  <div class="grid grid-cols-1 @md:grid-cols-2 @xl:grid-cols-4">
    <!-- responds to container width -->
  </div>
</div>

@container turns on container-type: inline-size, and variants like @md: and @xl: activate inside the container query, independent of viewport queries (md:, xl:). Modular cards, sidebars, and grids finally become truly container-driven.

5.2 color-mix() and automatic alpha

Blending colors in v3 meant adding extra tokens. v4 supports automatic alpha modifiers backed by color-mix().

<!-- 50% alpha of brand color -->
<div class="bg-brand-500/50">...</div>

<!-- Mix two colors 50:50 in OKLCH -->
<div class="bg-[color-mix(in_oklch,var(--color-brand-500),var(--color-accent-500))]">
  ...
</div>

The alpha modifier (/50) existed in v3, but v4 switches the default mixing space to OKLCH. Results look much more natural than blending in sRGB.

5.3 P3 color space

v3 defaulted to sRGB hex. v4's default palette is defined in OKLCH and renders more saturated colors on P3 displays.

@theme {
  /* The v4 default blue is already OKLCH */
  --color-blue-500: oklch(0.623 0.214 259.815);
}

Older monitors automatically fall back to sRGB. On a P3 display (recent MacBooks, iPads, iPhones), v4 colors look noticeably richer.

5.4 Real cascade layers

In v3, @layer base { ... } was a Tailwind-managed pseudo-layer. v4 uses real CSS cascade layers.

@layer theme, base, components, utilities;

Specificity conflicts resolve at the layer level. When you add your own styles inside @layer, the priority rules become unambiguous.

5.5 @starting-style discovery

@starting-style, which lands alongside CSS native View Transitions, works out of the box in v4.

@layer base {
  dialog[open] {
    opacity: 1;
    transform: scale(1);
    transition: opacity 200ms, transform 200ms;
  }
  @starting-style {
    dialog[open] {
      opacity: 0;
      transform: scale(0.95);
    }
  }
}

Enter animations without a line of JS.


Chapter 6 · v3 vs v4 — Side-by-Side Matrix

Itemv3v4
Engine languageJavaScriptRust (Oxide)
Lightning CSSseparate PostCSS pluginbuilt in
Config locationtailwind.config.js@theme in CSS
Source detectionexplicit content: []auto + @source
Primary build integrationPostCSSVite plugin
Full build perfbaselineabout 3.5x faster
Incremental perfbaselineabout 100x faster
Tokens equal CSS varspartialall tokens
Container queriesplugincore
OKLCH and P3 defaultsRGB defaultOKLCH default
@apply usagewidepossible but de-emphasized
Dark modedarkMode: 'class' or media@custom-variant dark
Browser requirementsnearly all modernSafari 16.4+, Chrome 111+, Firefox 128+
Migration toolnonenpx @tailwindcss/upgrade

Chapter 7 · Real Migration — v3 to v4

7.1 The automatic upgrade CLI

Tailwind ships an official upgrade CLI.

npx @tailwindcss/upgrade@latest

What it does.

  1. Bumps package.json deps to the v4 packages.
  2. Reads your tailwind.config.js and converts it to a CSS file with @theme plus @source directives.
  3. Replaces the @tailwind base; @tailwind components; @tailwind utilities; three-liner with @import "tailwindcss";.
  4. Patches some renamed classes (e.g., shadow-sm to shadow-xs).

Across about 12 production codebases, the automatic conversion handled roughly 85% of the work. The remaining 15% needed human review.

7.2 Cases that usually need hands-on work

7.2.1 Shadow and border scale changes

v4 shifts the shadow scale by one step.

  • shadow-sm is now smaller (close to the old shadow-xs)
  • shadow is closer to the old shadow-sm
  • A new shadow-xs exists

The upgrade tool does a bulk rename, but visual regressions are likely. Run your Storybook through a visual diff after the rename.

7.2.2 Arbitrary value syntax

The bg-[color:var(--brand)] style from v3 has a tighter form in v4.

<!-- v3 -->
<div class="bg-[color:var(--brand)] p-[length:calc(1rem+2px)]">

<!-- v4 -->
<div class="bg-(--brand) p-[calc(1rem+2px)]">

bg-(--brand) is a new shorthand for CSS variables. Not every arbitrary value can be auto-converted.

7.2.3 Stepping away from @apply

In v3, packing utilities into a component class with @apply was the dominant pattern.

/* v3 */
.btn-primary {
  @apply bg-blue-500 text-white px-4 py-2 rounded;
}

v4 supports @apply too. But it has two weaknesses.

  • Classes built with @apply skip automatic content detection, so if they are not referenced anywhere the engine sees, tree-shaking is awkward.
  • The v4 philosophy is to abstract via React or Vue components, not via CSS class names.

When you can, move to component abstractions.

// v4 idiomatic
export function ButtonPrimary({ children }: { children: React.ReactNode }) {
  return (
    <button className="bg-blue-500 text-white px-4 py-2 rounded">
      {children}
    </button>
  )
}

7.2.4 Plugin compatibility

The popular v3 plugins — @tailwindcss/typography, @tailwindcss/forms, tailwindcss-animate — all shipped v4-compatible releases. Some community plugins are still tied to v3 APIs. Scan the dependency tree before upgrading.

pnpm why -r tailwindcss

7.2.5 darkMode configuration

/* v4 darkMode — class strategy */
@custom-variant dark (&:where(.dark, .dark *));

The default is the media query (prefers-color-scheme: dark). To use the class strategy, add the one-liner above to your CSS.

7.3 Incremental migration for big monorepos

If you cannot do a single big-bang upgrade, here is the pattern that worked.

  1. New packages on v4. v3 and v4 packages can coexist through separate build pipelines.
  2. Isolate legacy packages. Keep the v3 PostCSS pipeline separate so v4 output is not polluted.
  3. Single source of design tokens. Export tokens as CSS variables; v3 reads them via theme.extend, v4 reads them directly in @theme.
  4. Automate visual regression. Storybook plus Chromatic, Loki, or Percy is the safety net you actually need.

Chapter 8 · v4's Limits — Honestly

8.1 Browser baseline

v4 requires.

  • Safari 16.4+ (March 2023)
  • Chrome 111+ (March 2023)
  • Firefox 128+ (July 2024)

This is because v4 leans on @layer, color-mix(), container queries, and other modern CSS. If you need to support IE11 or older Safari for B2B or enterprise traffic, v4 is effectively off the table.

8.2 Re-learning curve

A lot of v3 tribal knowledge (the tailwind.config.js shape, the plugin API, the content globbing tricks) needs relearning. If your team is five or more engineers all comfortable in v3, the retraining cost can outweigh the migration itself.

8.3 Design token pipeline gap

Style Dictionary, Theo, Figma Tokens — at GA none had v4-friendly output. A year later most do, but if you maintain your own token build, expect to touch it.

8.4 Heavy @apply codebases

If your team has built a wide layer of component classes via @apply in CSS, v4 still runs, but you are working against the grain. Moving to component abstractions is real work.

8.5 PostCSS plugin collisions

Because v4 uses Lightning CSS internally, running a PostCSS plugin that does the same thing (postcss-preset-env, autoprefixer) duplicates work or conflicts. Clean the chain.


Chapter 9 · When to Upgrade, When to Wait

9.1 Upgrade now if

  • New projects. Faster builds, simpler config, modern CSS native — no reason to start on v3.
  • Vite-based SPAs or full-stack apps. This is where v4 shines hardest.
  • Design system or UI teams where HMR speed is productivity-critical. Incremental builds at 100x are real.
  • B2C services that target only modern browsers. No compatibility ceiling.

9.2 Wait if

  • Enterprise apps with legacy browser SLAs. If Safari 15 and older Edge are even 1% of your traffic, tread carefully.
  • Teams in the middle of a design system overhaul. Doing both at once is a 2x risk multiplier.
  • Next.js Pages Router legacy apps. Deep PostCSS pipelines lower the ROI.
  • Code with hard dependencies on niche v3 plugins. If a plugin is not yet compatible, wait it out.

9.3 ROI math

Rough calculation.

  • Build time saved x builds per day x developers x 30 = monthly time saved.
  • Saved time x cost per hour = monthly dollar savings.

A real example. 25 devs, 80 builds per day each, 3.2 seconds saved per build → about 50,000 seconds per month, roughly 14 hours. But the real value is not arithmetic — it is HMR being instant so flow does not break.


Chapter 10 · Toolchain Around v4

CategoryToolv4 integration status
Linteslint-plugin-tailwindcssv4 classes from 0.6.x
Formatterprettier-plugin-tailwindcssv4-compatible from 0.6.x
IDEVS Code Tailwind CSS IntelliSense0.12 or higher
Visual regressionChromatic, Loki, Percybuild-independent (recommended)
UI kitsshadcn/ui, Tremor, Catalystshadcn/ui CLI ships v4 mode
Design tokensStyle Dictionary 4 with tokens.jsoninstant compat via CSS var output
HeadlessHeadless UI v2no dependency
Form package@tailwindcss/forms v0.6v4 compatible

Teams using shadcn/ui have the smoothest migration. The shadcn/ui CLI defaults to v4 templates today.


Chapter 11 · Gotchas Found in Production

11.1 Variable order inside @theme

If one variable references another inside @theme, declaration order matters.

@theme {
  --color-base: #3b82f6;
  /* OK — color-base is declared first */
  --color-primary: var(--color-base);
}

Reverse the order and the var() reference resolves to unset.

11.2 Collisions with global CSS variables

If you use a generic name like --color-primary in your design token library, it collides with Tailwind's token. Always keep Tailwind tokens under a clear prefix (--color-brand-*, --color-acme-*).

11.3 Dynamic classes in Server Components

A class name built dynamically in a server component can slip through auto-detection. Two ways to fix it.

  • Spell the full class name explicitly (no string interpolation).
// Bad — `bg-${color}-500` is not detected
const className = `bg-${color}-500`

// Good — full string per branch
const colorMap = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
}
const className = colorMap[color]
  • A safelist analog, declared in CSS.
/* Force these classes to be included */
@source inline("bg-red-500 bg-blue-500 bg-green-500");

11.4 The prose class shift

prose from @tailwindcss/typography 0.6 has a subtly different tone in v4. Blog and docs sites need visual regression checks here.

11.5 Storybook integration

Storybook 8+ with the Vite builder picks up @tailwindcss/vite natively. With the Webpack builder you go through the PostCSS path. New Storybook setups default to the Vite builder.


Epilogue — Adoption Checklist and Anti-Patterns

12.1 Adoption checklist

  • Browser requirements (Safari 16.4+) match your project's support matrix.
  • All Tailwind plugins you depend on have v4-compatible versions.
  • autoprefixer removed from the PostCSS chain.
  • Visual regression tests are enabled in CI.
  • Design token build outputs CSS variables.
  • The output of the upgrade CLI is reviewed in a single PR, not blindly merged.
  • If you use darkMode: 'class', @custom-variant dark is added correctly.
  • Arbitrary value syntax (bg-[color:var(--x)] to bg-(--x)) is normalized.

12.2 Anti-patterns

  • Over-using @apply. Carrying v3 patterns straight over. Against the grain of v4.
  • Running both PostCSS plugin and Vite plugin. Pick one. Both equals double builds.
  • Leaving autoprefixer. v4 already prefixes. Remove it.
  • Keeping tailwind.config.js after upgrade. The CLI moved it into CSS; delete the old JS to avoid confusion.
  • Trying to reintroduce the content array. v4 uses a different mechanism. Use @source.
  • Expressing every design token as an arbitrary value. v4 wants you to give them names in @theme.
  • Partial v4 across a monorepo. If shared packages are consumed by both v3 and v4 apps, classes will collide somewhere.

12.3 What's next

The next post in the series tackles building a design system from scratch with shadcn/ui v2 plus Tailwind v4 — a real case study that ties together tokens, accessibility, dark mode, theme swap, and Server Component compatibility.


References

현재 단락 (1/326)

On January 22, 2025, Tailwind CSS v4.0 shipped as GA. The first line of the release post was blunt: ...

작성 글자: 0원문 글자: 19,547작성 단락: 0/326