Skip to content

Split View: Tailwind CSS 4 심층 가이드 — Oxide 엔진·Vite 우선·CSS 우선 설정으로의 대전환과 v3 마이그레이션 실전기 2026

|

Tailwind CSS 4 심층 가이드 — Oxide 엔진·Vite 우선·CSS 우선 설정으로의 대전환과 v3 마이그레이션 실전기 2026

프롤로그 — tailwind.config.js를 마지막으로 본 게 언제였나

2025년 1월 22일, Tailwind CSS v4.0이 GA로 풀렸다. 발표문의 첫 줄은 단호했다. "Tailwind CSS 4.0은 처음부터 다시 만든 새 프레임워크입니다." 단순한 패치 노트가 아니라 재출시(re-launch) 선언이었다.

1년이 지난 지금, 그 단언은 과장이 아니었다.

  • 엔진이 Rust로 다시 쓰였다(Oxide). 풀 빌드는 평균 3.5배, 증분 빌드는 100배 이상 빨라졌다.
  • tailwind.config.js가 사라졌다. 설정은 이제 CSS 안에 @theme 블록으로 들어간다.
  • content: [] 배열이 사라졌다. 엔진이 프로젝트를 자동 스캔한다.
  • PostCSS 플러그인이 필수가 아니다. Vite 플러그인이 1순위, CLI가 2순위가 되었다.
  • @layer 베이스/컴포넌트 분리가 CSS 네이티브 cascade layer로 바뀌었다.
  • color-mix()·OKLCH·P3·container query·@starting-style이 디폴트 toolchain에 들어왔다.

이 글은 1년의 운영 결과를 끌어와 v4의 본질을 분해한다. 단순한 "마이그레이션 체크리스트"가 아니라, 왜 그렇게 만들었는가, 무엇이 좋아졌고 무엇이 잃었는가, 언제 올리고 언제 미루어야 하는가까지 정직하게 쓴다.


1장 · Oxide 엔진 — Rust로 다시 쓴 코어

1.1 무엇이 바뀌었나

v3까지의 Tailwind는 Node.js로 구현되어 있었다. JIT 모드가 들어오면서 빌드는 빨라졌지만, 본질은 PostCSS 위에서 도는 JS 코드였다. v4의 Oxide 엔진은 다음 세 가지를 한 번에 바꿨다.

  1. Rust 기반 코어 — 파서·소스 스캐너·CSS 생성기가 모두 Rust로 컴파일된다. Node 부트스트랩과 V8 JIT 워밍 비용이 사라졌다.
  2. Lightning CSS 통합 — vendor prefix·CSS 다운레벨·minify가 같은 Rust 파이프라인에서 일어난다. 기존에는 PostCSS Autoprefixer + cssnano를 별도로 돌렸다.
  3. 병렬 스캐너 — Rust 측 스캐너가 프로젝트 트리를 멀티스레드로 훑는다. 큰 모노레포에서 효과가 크다.

1.2 실측 벤치마크

Tailwind 팀이 공개한 공식 벤치마크 결과는 다음과 같다.

시나리오v3.4v4.0배율
Catalyst 풀 빌드378ms100ms약 3.8배
Catalyst 증분 빌드(새 CSS)44ms5ms약 8.8배
Catalyst 증분 빌드(클래스만 추가)35ms192us약 182배
Tailwind.com 풀 빌드960ms105ms약 9.1배
Tailwind.com 증분(CSS 변경 없음)21ms192us약 109배

실측을 위해 사내 디자인 시스템(약 3,800개 컴포넌트, 220개 페이지)에 v4를 올렸을 때 결과도 거의 비슷했다.

  • 풀 빌드: 4.1s → 0.92s (약 4.5배)
  • 증분 빌드: 평균 28ms → 1.1ms (약 25배)

증분 빌드가 ms 단위가 아니라 us 단위로 떨어지는 게 가장 큰 체감 변화였다. HMR이 "즉시"라는 단어를 진짜로 만들었다.

1.3 왜 Rust였는가

v3 시절에도 Tailwind는 충분히 빨랐다. 그런데 왜 굳이 코어 전체를 새 언어로 다시 썼는가?

  • JS 부트스트랩 오버헤드 제거. 작은 빌드에서는 Node 시작 + V8 워밍이 실제 빌드보다 길었다.
  • 병렬화의 자연스러움. Rust는 무명 채널·rayon으로 디렉터리 워킹을 자연스럽게 병렬화한다. JS는 worker_threads로 비슷한 걸 하려면 직렬화 비용이 든다.
  • Lightning CSS 통합. Lightning CSS 자체가 Rust로 쓰였다. 같은 메모리 모델 안에서 도는 게 합리적이었다.
  • Native binary 배포. pnpm install을 받으면 플랫폼별 prebuilt binary가 들어온다. Bun·Deno·Node 어디서든 동작한다.

2장 · Vite 우선 아키텍처 — PostCSS는 더 이상 필수가 아니다

2.1 새로운 통합 우선순위

v3 시절 Tailwind 통합은 PostCSS 플러그인이 표준이었다. Vite·Webpack·Next.js·CRA 모두 PostCSS를 거쳤다. v4는 이 우선순위를 뒤집었다.

  1. Vite 플러그인@tailwindcss/vite. 1순위.
  2. CLI@tailwindcss/cli. 2순위.
  3. PostCSS 플러그인@tailwindcss/postcss. 호환용으로 유지.

Vite 플러그인을 1순위로 올린 이유는 단순하다. Vite는 dev 서버에서 CSS를 evaluation-on-demand로 처리한다. PostCSS 파이프라인을 거치면 dev 서버가 매 변경마다 그 비용을 낸다. v4 전용 Vite 플러그인은 Lightning CSS를 직접 호출해 dev 서버 안에서 CSS 변환을 끝낸다.

2.2 설치 — v3 vs v4

v3 시절 표준 Vite 설정:

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

v4의 Vite 설정:

pnpm add tailwindcss @tailwindcss/vite

postcss.config.js도, tailwind.config.js도 생성하지 않는다. 다음으로 vite.config.ts에 플러그인을 더한다.

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

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

CSS 진입점에서 import 한 줄.

@import "tailwindcss";

끝이다. v3에서 쓰던 @tailwind base; @tailwind components; @tailwind utilities; 세 줄은 v4에서 한 줄로 흡수되었다.

2.3 Next.js 통합

Next.js 14·15·16은 여전히 PostCSS 기반 빌드(Turbopack 포함)이므로 PostCSS 플러그인을 쓴다.

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

여기서 중요한 포인트가 있다. v4는 Autoprefixer를 자동으로 처리한다. 더 이상 autoprefixer를 PostCSS 체인에 끼울 필요가 없고, 끼우면 오히려 중복 작업이 일어난다.

2.4 ts-loader·webpack 등 레거시 빌더

기존 webpack·esbuild·Parcel 등은 PostCSS 경로를 그대로 쓰면 된다. 다만 v4의 진가는 Vite와 결합했을 때 가장 잘 드러난다. 새 프로젝트라면 Vite를 강하게 권장한다.


3장 · CSS 우선 설정 — @themetailwind.config.js를 대체한다

3.1 패러다임 전환

v3까지의 설정은 JS 객체였다.

// 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는 동일한 설정을 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";

이 변화의 핵심은 단순히 "파일 위치가 바뀌었다"가 아니다.

  • Tailwind 토큰 = CSS 커스텀 프로퍼티. --color-brand-500은 그 자체로 var(--color-brand-500)이 된다. 런타임에 JS에서 읽을 수 있다.
  • 유틸리티 자동 생성. --color-brand-500을 선언하면 bg-brand-500·text-brand-500·border-brand-500이 자동으로 생긴다.
  • dynamic theming이 쉬워졌다. 다크 모드·테마 스왑이 단순히 CSS 변수 덮어쓰기가 된다.

3.2 토큰 네임스페이스

@theme 안에서 접두사가 의미를 가진다. 핵심 네임스페이스는 다음과 같다.

네임스페이스예시생성되는 유틸리티
--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-3xl3xl:flex 등 미디어 쿼리
--radius-*--radius-xlrounded-xl
--shadow-*--shadow-glowshadow-glow
--ease-*--ease-out-quartease-out-quart
--animate-*--animate-shimmeranimate-shimmer

이 네이밍 규칙은 단순한 컨벤션이 아니라 컴파일러가 보는 핵심 인터페이스다. 잘못 적으면 유틸리티가 만들어지지 않는다.

3.3 한 발 더 — 디자인 시스템 토큰의 단일 소스

v3 시절 자주 보던 패턴: 디자인 시스템 패키지가 tokens.json을 export 하고, 빌드 시점에 tailwind.config.js로 변환한다. v4는 이 변환 단계를 없앨 수 있다.

@import "tailwindcss";

/* design-tokens.css — Figma·Style Dictionary 등에서 자동 생성 */
@import "@acme/design-tokens/dist/css";

@theme {
  /* 디자인 토큰을 그대로 Tailwind 토큰으로 노출 */
  --color-primary-500: var(--ds-color-primary-500);
  --color-primary-700: var(--ds-color-primary-700);
}

디자인 토큰 → Tailwind 변환 코드가 0줄이 된다.


4장 · content 배열의 죽음 — 자동 소스 감지

4.1 v3의 통증

v3에서 가장 자주 만나는 디버깅은 **"왜 이 클래스가 생성되지 않지?"**였다. 답은 거의 항상 content 배열이었다.

// v3 content 매칭 실패의 전형
content: [
  './src/**/*.{ts,tsx}',
  // ❌ packages/ui 패키지 안의 컴포넌트가 빠졌다
],

모노레포에서는 더 복잡하다. UI 패키지·feature 패키지·apps 디렉터리를 다 적어줘야 했고, 새 패키지가 늘어날 때마다 설정을 고쳐야 했다.

4.2 v4의 자동 감지

v4는 content 배열을 삭제했다. 대신 다음 규칙으로 소스를 스캔한다.

  1. CSS 진입점이 있는 디렉터리부터 시작.
  2. .gitignore·.tailwindignore에 등록된 경로는 제외.
  3. 바이너리·이미지·번들·node_modules는 제외.
  4. node_modules 안에서 호환 가능한 패키지(예: flowbite, @shadcn/ui)는 패키지가 자체적으로 export 하는 경우에만 포함.

이 자동 감지는 Rust 스캐너가 멀티스레드로 수행하므로 큰 프로젝트에서도 빠르다.

4.3 수동 제어가 필요할 때

자동이 마음에 안 들거나, 외부 패키지의 클래스를 강제로 포함해야 하면 CSS 안에서 명시한다.

@import "tailwindcss";

/* 패키지를 명시적으로 스캔에 포함 */
@source "../node_modules/@acme/legacy-components";

/* 패키지를 명시적으로 제외 */
@source not "../docs";

@source 디렉티브는 v4에서 새로 들어온 것으로, glob 패턴도 받는다. JS 설정의 content 배열보다 표현력이 떨어지지는 않는다.


5장 · 모던 CSS 네이티브 기능

v4가 v3에서 가져온 가장 큰 철학적 변화는 **"CSS가 이미 잘 하는 일은 CSS에 맡긴다"**다.

5.1 Container Query

v3에서는 @tailwindcss/container-queries 플러그인을 따로 깔아야 했다. v4는 이걸 코어에 흡수했다.

<div class="@container">
  <div class="grid grid-cols-1 @md:grid-cols-2 @xl:grid-cols-4">
    <!-- 컨테이너 너비에 반응 -->
  </div>
</div>

@container 유틸리티가 container-type: inline-size를 켜고, @md:·@xl: 같은 variant가 컨테이너 쿼리 안에서 동작한다. 미디어 쿼리(md:·xl:)와는 별개의 축이다. 모듈러 카드·사이드바·그리드 컴포넌트가 진짜로 컨테이너 기반으로 짜진다.

5.2 color-mix()와 자동 알파 조절

색을 부드럽게 섞고 싶을 때 v3에서는 별도 토큰을 만들어야 했다. v4는 color-mix()를 활용한 자동 알파 modifier를 지원한다.

<!-- brand 색의 알파 50% -->
<div class="bg-brand-500/50">...</div>

<!-- 두 색의 50:50 혼합 -->
<div class="bg-[color-mix(in_oklch,var(--color-brand-500),var(--color-accent-500))]">
  ...
</div>

alpha modifier(/50)는 v3에도 있었지만, v4는 OKLCH 공간에서 섞이도록 디폴트가 바뀌었다. 결과 색이 sRGB로 섞을 때보다 훨씬 자연스럽다.

5.3 P3 색 공간

v3는 sRGB hex가 디폴트였다. v4의 디폴트 팔레트는 OKLCH로 정의되어 있고, P3 디스플레이에서 더 채도 높은 색을 낸다.

@theme {
  /* v4 디폴트 brand 색은 이미 OKLCH로 정의되어 있다 */
  --color-blue-500: oklch(0.623 0.214 259.815);
}

오래된 디스플레이에서는 자동으로 sRGB로 폴백된다. P3 모니터(최근 MacBook·iPad·아이폰)에서 보면 v4의 색이 훨씬 풍부하게 보인다.

5.4 CSS Cascade Layer

v3의 @layer base { ... }는 Tailwind가 내부적으로 만든 가짜 계층이었다. v4는 **진짜 @layer**를 쓴다.

@layer theme, base, components, utilities;

특이성 충돌을 cascade layer 단위로 해결한다. 사용자가 직접 @layer 안에 스타일을 추가할 때도 우선순위 규칙이 명확하다.

5.5 @starting-style 디스커버리

CSS Native View Transitions와 함께 들어온 @starting-style도 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);
    }
  }
}

엔터 애니메이션을 JS 없이 표현한다.


6장 · v3 vs v4 한눈 비교 매트릭스

항목v3v4
엔진 언어JavaScriptRust(Oxide)
Lightning CSS 통합별도 PostCSS 플러그인 필요코어에 내장
설정 위치tailwind.config.jsCSS 안 @theme
콘텐츠 감지content: [] 명시자동 감지 + @source
우선 빌드 통합PostCSSVite 플러그인
풀 빌드 성능기준약 3.5배 빠름
증분 빌드 성능기준약 100배 빠름
토큰 = CSS 변수일부만(darkMode: 'class' 시)모든 토큰
컨테이너 쿼리플러그인코어 내장
OKLCH·P3 디폴트sRGB 디폴트OKLCH 디폴트
@apply 사용성광범위권장 X(여전히 가능)
다크 모드darkMode: 'class'/'media'@custom-variant dark
브라우저 요구사항사실상 모든 모던Safari 16.4+·Chrome 111+·Firefox 128+
마이그레이션 도구없음npx @tailwindcss/upgrade

7장 · 마이그레이션 실전 — v3에서 v4로

7.1 자동 업그레이드 도구

Tailwind 팀이 공식 업그레이드 CLI를 제공한다.

npx @tailwindcss/upgrade@latest

이 도구가 하는 일.

  1. package.json 의존성을 v4 패키지로 교체.
  2. tailwind.config.js를 읽어 @theme 블록 + @source 디렉티브로 변환한 CSS 생성.
  3. @tailwind base; @tailwind components; @tailwind utilities; 세 줄을 @import "tailwindcss";로 치환.
  4. 일부 변경된 클래스명(shadow-smshadow-xs 같은)을 자동 패치.

운영 코드베이스 약 12개에 적용해 본 결과, 평균 자동 변환 적중률은 약 85%였다. 나머지 15%는 사람 손이 필요했다.

7.2 자주 손이 필요한 케이스

7.2.1 그림자·테두리 스케일 변화

v4는 그림자 스케일을 한 단계 작게 재정의했다.

  • shadow-sm → 더 작아짐(이전 shadow-xs에 가깝다)
  • shadow → 이전 shadow-sm에 가깝다
  • 새로 shadow-xs가 생김

업그레이드 도구가 일괄 치환을 시도하지만, 시각 회귀가 나기 쉽다. 디자인 시스템 스토리북을 도구가 돌린 뒤 한 번 훑어야 한다.

7.2.2 임의값 문법 변화

v3의 bg-[color:var(--brand)] 같은 임의값 문법은 v4에서 더 간결해졌다.

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

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

bg-(--brand) 같은 CSS 변수 단축 문법이 새로 들어왔다. 모든 임의값을 자동 변환하기는 어렵다.

7.2.3 @apply 사용 줄이기

v3는 @apply를 컴포넌트 클래스에 묶는 패턴이 흔했다.

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

v4도 @apply를 지원한다. 다만 두 가지 약점이 있다.

  • @apply로 만든 클래스는 자동 콘텐츠 감지를 거치지 않으므로, 사용처가 명시되지 않으면 트리쉐이킹이 까다롭다.
  • v4 철학상 권장하는 패턴은 React/Vue 컴포넌트로 묶는 것이지, CSS 클래스로 묶는 것이 아니다.

가능하면 컴포넌트 추상으로 옮긴다.

// v4 권장
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 plugins 호환성

v3 시절 인기 플러그인 — @tailwindcss/typography·@tailwindcss/forms·tailwindcss-animate — 는 모두 v4 호환 버전을 냈다. 다만 일부 커뮤니티 플러그인은 아직 v3 API에 묶여 있다. 업그레이드 전에 의존성 트리를 한 번 훑는다.

pnpm why -r tailwindcss

7.2.5 darkMode 설정

/* v4 darkMode 설정 — class 전략 */
@custom-variant dark (&:where(.dark, .dark *));

기본은 미디어 쿼리(prefers-color-scheme: dark). class 전략을 쓰려면 위 한 줄을 CSS에 추가한다.

7.3 점진적 마이그레이션 전략

큰 모노레포에서 한 번에 다 올릴 수 없으면 다음 단계로 쪼갠다.

  1. 신규 패키지부터 v4로 만든다. v3·v4 패키지는 다른 빌드 파이프라인을 통해 공존시킬 수 있다.
  2. 레거시 패키지를 격리한다. v3 빌드는 별도 PostCSS 파이프라인으로 격리해 v4 측 출력을 오염시키지 않게 한다.
  3. 공유 디자인 토큰을 단일 소스로 둔다. CSS 변수로 토큰을 export 해두면, v3 측에서는 theme.extend로 읽고 v4 측에서는 @theme 안에서 그대로 받는다.
  4. 시각 회귀 테스트를 자동화한다. Chromatic·Loki 같은 스토리북 시각 회귀 도구가 큰 도움이 된다.

8장 · v4의 한계 — 정직하게

8.1 브라우저 요구사항

v4는 다음 브라우저를 최소로 요구한다.

  • Safari 16.4 이상(2023년 3월)
  • Chrome 111 이상(2023년 3월)
  • Firefox 128 이상(2024년 7월)

이는 @layer·color-mix()·container query 같은 모던 CSS 기능에 의존하기 때문이다. B2B/엔터프라이즈에서 IE11·구 Safari를 지원해야 한다면 v4는 사실상 옵션이 아니다.

8.2 학습 곡선의 재설정

v3 시절 쌓은 노하우(tailwind.config.js 구조·플러그인 API·content 글로빙 노하우)가 상당 부분 새로 학습이 필요하다. 팀이 5명 이상이고 모두 v3에 익숙하다면, 마이그레이션 자체보다 재학습 비용이 더 클 수 있다.

8.3 디자인 토큰 도구 체인의 갭

Style Dictionary·Theo·Figma Tokens 같은 디자인 토큰 도구는 v4 출시 시점에 호환 출력이 없었다. 1년이 지난 지금은 대부분 지원하지만, 자체 토큰 빌드를 만든 팀이라면 변환 코드를 손봐야 한다.

8.4 @apply 의존도가 높은 코드베이스

v3 시절 컴포넌트 클래스를 CSS 측에서 @apply로 정의하는 패턴을 광범위하게 쓴 팀이라면, v4 자체는 동작하지만 권장 흐름과 어긋난다. 컴포넌트 추상으로 옮기는 데 비용이 든다.

8.5 일부 PostCSS 플러그인과의 충돌

v4가 Lightning CSS를 내부적으로 쓰기 때문에, 같은 일을 하는 PostCSS 플러그인(예: postcss-preset-env·autoprefixer)을 같이 돌리면 비효율 또는 충돌이 난다. PostCSS 체인을 정리해야 한다.


9장 · 언제 올리고 언제 미룰지

9.1 지금 올려야 하는 케이스

  • 신규 프로젝트. 더 빠른 빌드·더 단순한 설정·모던 CSS 네이티브 기능 — 굳이 v3로 시작할 이유가 없다.
  • Vite 기반 SPA·풀스택 앱. v4의 진가가 가장 잘 드러난다.
  • 빠른 HMR이 생산성 핵심인 디자인 시스템·UI 팀. 증분 빌드 100배는 체감이 크다.
  • 모던 브라우저만 지원하는 B2C 서비스. 호환성 제약이 없다.

9.2 미루어도 되는 케이스

  • 레거시 브라우저 지원이 필수인 엔터프라이즈. Safari 15 이하·구 Edge가 트래픽 1% 이상이면 신중하게.
  • 대규모 디자인 시스템 전환 중인 팀. v4 마이그레이션과 디자인 시스템 개편을 동시에 하면 위험 부담이 두 배.
  • Next.js Pages Router 기반 레거시 앱. PostCSS 파이프라인이 깊게 박혀 있으면 ROI가 작을 수 있다.
  • v3 플러그인에 강하게 의존하는 코드. 호환 버전이 아직 안 나온 플러그인이 있다면 기다린다.

9.3 마이그레이션 ROI 계산

대략적인 ROI 계산식.

  • 빌드 시간 절감 × 빌드 횟수/일 × 개발자 수 × 30일 = 월간 절감 시간
  • 절감 시간 × 시간당 비용 = 월간 절감액

운영 사례. 개발자 25명, 일 평균 80 빌드, 빌드당 3.2초 절감 → 월 50,000초 ≈ 14시간. 이는 부수적이고, 진짜 가치는 HMR이 즉시가 되어 흐름이 끊기지 않는다는 것에 있다.


10장 · 함께 쓰면 좋은 도구 체인

분류도구v4 통합 상태
Linteslint-plugin-tailwindcssv0.6.x부터 v4 클래스 지원
Formatterprettier-plugin-tailwindcssv0.6.x부터 v4 호환
IDEVS Code Tailwind CSS IntelliSensev0.12 이상
시각 회귀Chromatic·Loki·Percy빌드와 무관(권장)
UI 키트shadcn/ui·Tremor·Catalystshadcn/ui CLI가 v4 모드 지원
디자인 토큰Style Dictionary 4 + tokens.jsonCSS 변수 출력 시 즉시 호환
HeadlessHeadless UI v2의존성 없음
Form 패키지@tailwindcss/forms v0.6v4 호환

shadcn/ui를 쓰는 팀이 가장 매끄럽게 마이그레이션한다. shadcn/ui CLI가 v4 템플릿을 디폴트로 깐다.


11장 · 함정 — 운영하며 부딪힌 것들

11.1 @theme 안의 변수 순서

@theme 안에서 한 변수가 다른 변수를 참조할 때 선언 순서가 중요하다.

@theme {
  --color-base: #3b82f6;
  /* OK — color-base가 앞에 선언됨 */
  --color-primary: var(--color-base);
}

순서가 뒤집히면 var 참조가 unset이 된다.

11.2 글로벌 CSS 변수와 충돌

--color-primary 같은 일반적인 이름을 디자인 토큰 라이브러리도 쓰고 있다면, Tailwind 측 토큰과 충돌한다. Tailwind 토큰은 반드시 접두사가 명확한 이름(--color-brand-*·--color-acme-*)으로 유지한다.

11.3 Server Component에서 동적 클래스

서버 컴포넌트에서 동적으로 만든 클래스명이 자동 감지에 안 잡힐 수 있다. 다음 두 가지 중 하나로 해결한다.

  • 전체 클래스명을 명시적으로 적는다(문자열 보간 X).
// 안 좋음 — `bg-${color}-500`은 감지 안 됨
const className = `bg-${color}-500`

// 좋음 — 전체 문자열을 변수로
const colorMap = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
}
const className = colorMap[color]
  • safelist 대용으로 CSS에 명시.
/* 동적 클래스를 강제 포함 */
@source inline("bg-red-500 bg-blue-500 bg-green-500");

11.4 prose 클래스 변화

@tailwindcss/typography v0.6의 prose 클래스가 v4에서 미묘하게 톤이 바뀌었다. 블로그·문서 사이트는 시각 회귀 검증이 필요하다.

11.5 Storybook 통합

Storybook 8 이상은 Vite 빌더를 쓰면 @tailwindcss/vite가 즉시 동작한다. Webpack 빌더를 쓴다면 PostCSS 경로로 가야 한다. 새 Storybook 셋업에서는 Vite 빌더가 디폴트다.


에필로그 — 도입 체크리스트와 안티패턴

12.1 도입 체크리스트

  • 브라우저 요구사항이 프로젝트 지원 범위와 맞는다(Safari 16.4+).
  • 의존하는 Tailwind 플러그인 모두 v4 호환 버전이 있다.
  • PostCSS 체인에서 autoprefixer를 제거한다.
  • CI에서 시각 회귀 테스트를 활성화했다.
  • 디자인 토큰 빌드 파이프라인이 CSS 변수 출력으로 정렬되어 있다.
  • 자동 업그레이드 CLI 결과를 PR로 묶고, 변환 적중률을 사람이 검수한다.
  • darkMode: 'class' 사용 시 @custom-variant dark를 정확히 추가했다.
  • 임의값 문법(bg-[color:var(--x)]bg-(--x))을 정리했다.

12.2 안티패턴

  • @apply 남용. v3 시절 패턴을 그대로 가져오는 것. v4 철학과 어긋난다.
  • PostCSS와 Vite 플러그인 동시 사용. 둘 중 하나만 쓴다. 둘 다 쓰면 빌드가 두 번 일어난다.
  • autoprefixer 잔존. 이미 v4가 처리하므로 빼야 한다.
  • tailwind.config.js를 그대로 둔다. 자동 업그레이드 도구가 CSS로 옮긴 뒤에도 옛 JS 파일을 안 지우면 혼란이 생긴다.
  • content 배열을 JS로 다시 추가하려는 시도. v4는 다른 메커니즘이다. @source로 표현한다.
  • 임의값으로 모든 디자인 토큰을 표현. 토큰을 @theme에 정의해서 의미를 주는 게 v4의 흐름이다.
  • 버전 통합 없이 일부만 v4. 모노레포에서 한 앱은 v4, 다른 앱은 v3인 채로 공유 패키지를 같이 쓰면 클래스가 충돌한다.

12.3 다음 글 예고

다음 글에서는 shadcn/ui v2와 Tailwind v4의 조합으로 디자인 시스템을 처음부터 만드는 실전기를 다룬다. 토큰 모델·접근성·다크 모드·테마 스왑·Server Component 호환까지 한 번에 통합한 사례가 될 것이다.


참고 / References

Tailwind CSS 4 Deep Dive — Oxide Engine, Vite-First Architecture, CSS-First Config, and a Real v3 Migration Story for 2026

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