跳至内容
·7 分钟阅读

Tailwind CSS v4:到底改了什么,是否该迁移

CSS 优先配置、@layer 集成、内置容器查询、新引擎性能、破坏性变更,以及我从 v3 迁移到 v4 的真实体验。

分享:X / TwitterLinkedIn

我从 v1.x 就开始用 Tailwind CSS 了,那时候社区一半人觉得它是个怪胎,另一半人已经靠它疯狂出活了。每个大版本都是一次重大飞跃,但 v4 不一样。它不仅仅是一个功能版本,而是一次从头开始的架构重写,改变了你和框架之间的根本契约。

把两个生产项目从 v3 迁移到 v4,又在 v4 上从零开始了三个新项目之后,我对什么是真正的改进、什么还粗糙、以及你今天是否应该迁移,有了清晰的判断。不吹不黑——只讲我观察到的。

全局视角:v4 到底是什么#

Tailwind CSS v4 同时是三件事:

  1. 全新引擎 — 从 JavaScript 重写为 Rust(Oxide 引擎),构建速度大幅提升
  2. 全新配置范式 — CSS 优先配置替代 tailwind.config.js 成为默认方式
  3. 更紧密的 CSS 平台集成 — 原生 @layer、容器查询、@starting-style 和级联层成为一等公民

你到处看到的标题是"快 10 倍"。这是真的,但它低估了实际的变化。配置和扩展 Tailwind 的心智模型已经根本性地转变了。你现在是在用 CSS 工作,而不是用一个生成 CSS 的 JavaScript 配置对象。

以下是一个最简的 Tailwind v4 设置:

css
/* app.css — 这就是全部设置 */
@import "tailwindcss";

就这样。没有配置文件。没有 PostCSS 插件配置(对大多数场景而言)。没有 @tailwind base; @tailwind components; @tailwind utilities; 指令。一行导入,就能跑。

对比 v3:

css
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
js
// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
js
// v3 — postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

三个文件精简为一行。这不仅仅是少了样板代码——是减少了配置出错的可能性。在 v4 中,内容检测是自动的,它会自动扫描你的项目文件,无需你拼写 glob 模式。

CSS 优先配置与 @theme#

这是最大的概念转变。在 v3 中,你通过 JavaScript 配置对象自定义 Tailwind:

js
// v3 — tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#eff6ff",
          500: "#3b82f6",
          900: "#1e3a5f",
        },
      },
      fontFamily: {
        display: ["Inter Variable", "sans-serif"],
      },
      spacing: {
        18: "4.5rem",
        112: "28rem",
      },
      borderRadius: {
        "4xl": "2rem",
      },
    },
  },
};

在 v4 中,所有这些都用 @theme 指令写在 CSS 里:

css
@import "tailwindcss";
 
@theme {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
 
  --font-display: "Inter Variable", sans-serif;
 
  --spacing-18: 4.5rem;
  --spacing-112: 28rem;
 
  --radius-4xl: 2rem;
}

一开始我是抗拒的。我喜欢有一个 JavaScript 对象能看到整个设计系统。但用了一周 CSS 方式后,我因为三个原因改变了想法:

1. 原生 CSS 自定义属性自动暴露。 你在 @theme 中定义的每个值都成为 :root 上的 CSS 自定义属性。这意味着你的主题值在纯 CSS、CSS Modules、<style> 标签里,任何 CSS 运行的地方都能访问:

css
/* 这是免费获得的 */
:root {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
}
css
/* 任何地方都能用——不需要 Tailwind */
.custom-element {
  border: 2px solid var(--color-brand-500);
}

2. 你可以在 @theme 中使用 CSS 特性。 媒体查询、light-dark()calc()——真正的 CSS 在这里能用,因为它本身就是真正的 CSS:

css
@theme {
  --color-surface: light-dark(#ffffff, #0a0a0a);
  --color-text: light-dark(#0a0a0a, #fafafa);
  --spacing-container: calc(100vw - 2rem);
}

3. 和你的其他 CSS 放在一起。 你的主题、自定义工具类和基础样式全都用同一种语言,想放同一个文件也行。不用在"CSS 世界"和"JavaScript 配置世界"之间来回切换。

覆盖 vs 扩展默认主题#

在 v3 中你有 theme(替换)和 theme.extend(合并)。在 v4 中,心智模型不同:

css
@import "tailwindcss";
 
/* 这是扩展默认主题——在现有颜色旁边添加品牌色 */
@theme {
  --color-brand-500: #3b82f6;
}

如果你想完全替换一个命名空间(比如移除所有默认颜色),用 @theme 配合 --color-* 通配符重置:

css
@import "tailwindcss";
 
@theme {
  /* 先清除所有默认颜色 */
  --color-*: initial;
 
  /* 然后只定义你的颜色 */
  --color-white: #ffffff;
  --color-black: #000000;
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
}

这种通配符重置模式很优雅。你精确选择默认主题的哪些部分保留、哪些替换。想保留所有默认间距但用自定义颜色?重置 --color-*: initial; 然后不动间距就行。

多主题文件#

对于更大的项目,你可以把主题拆分到多个文件:

css
/* styles/theme/colors.css */
@theme {
  --color-brand-50: #eff6ff;
  --color-brand-100: #dbeafe;
  --color-brand-200: #bfdbfe;
  --color-brand-300: #93c5fd;
  --color-brand-400: #60a5fa;
  --color-brand-500: #3b82f6;
  --color-brand-600: #2563eb;
  --color-brand-700: #1d4ed8;
  --color-brand-800: #1e40af;
  --color-brand-900: #1e3a5f;
  --color-brand-950: #172554;
}
 
/* styles/theme/typography.css */
@theme {
  --font-display: "Inter Variable", sans-serif;
  --font-body: "Source Sans 3 Variable", sans-serif;
  --font-mono: "JetBrains Mono Variable", monospace;
 
  --text-display: 3.5rem;
  --text-display--line-height: 1.1;
  --text-display--letter-spacing: -0.02em;
}
css
/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";

这比 v3 那种把巨大的 tailwind.config.js 拆分或试图用 require() 的模式干净多了。

Oxide 引擎:真的快了 10 倍#

Tailwind v4 的引擎是用 Rust 完全重写的,他们叫它 Oxide。我对"快 10 倍"的说法持怀疑态度——营销数字很少经得起真实项目的检验。所以我做了基准测试。

我的测试项目: 一个 Next.js 应用,847 个组件,142 个页面,大约 23,000 处 Tailwind 类名使用。

指标v3 (Node)v4 (Oxide)提升
首次构建4,280ms387ms11x
增量(编辑 1 个文件)340ms18ms19x
全量重建(清理后)5,100ms510ms10x
开发服务器启动3,200ms290ms11x

对我的项目来说,"10 倍"的说法是保守的。增量构建才是真正闪光的地方——18ms 意味着基本上是即时的。你保存文件,浏览器在你切换标签页之前就已经有了新样式。

为什么这么快?#

三个原因:

1. Rust 代替 JavaScript。 核心 CSS 解析器、类名检测和代码生成全是原生 Rust。这不是"为了好玩重写成 Rust"的情况——CSS 解析确实是 CPU 密集型工作,原生代码相比 V8 有巨大优势。

2. 热路径不经过 PostCSS。 在 v3 中,Tailwind 是一个 PostCSS 插件。每次构建意味着:将 CSS 解析为 PostCSS AST,运行 Tailwind 插件,序列化回 CSS 字符串,然后其他 PostCSS 插件运行。在 v4 中,Tailwind 有自己的 CSS 解析器,直接从源码到输出。PostCSS 仍然支持兼容性,但主路径完全跳过它。

3. 更智能的增量处理。 新引擎积极缓存。当你编辑单个文件时,它只重新扫描该文件的类名,只重新生成变化的 CSS 规则。v3 引擎在这方面比人们认为的更聪明(JIT 模式已经是增量的了),但 v4 通过细粒度的依赖追踪走得更远。

速度真的重要吗?#

重要,但不是你想的那个原因。对大多数项目来说,v3 的构建速度"还行"。开发时等几百毫秒,不痛苦。

v4 的速度之所以重要,是因为它让 Tailwind 在你的工具链中变得隐形。当构建在 20ms 以内时,你不再把 Tailwind 当作一个构建步骤来思考了。它变得像语法高亮一样——永远在那里,从不碍事。在一整天的开发中,这种心理差异是很显著的。

原生 @layer 集成#

在 v3 中,Tailwind 使用自己的层系统,有 @layer base@layer components@layer utilities。它们看起来像 CSS 级联层但不是——它们是 Tailwind 特有的指令,控制生成的 CSS 出现在输出中的位置。

在 v4 中,Tailwind 使用真正的 CSS 级联层:

css
/* v4 输出 — 简化版 */
@layer theme, base, components, utilities;
 
@layer base {
  /* 重置,preflight */
}
 
@layer components {
  /* 你的组件类 */
}
 
@layer utilities {
  /* 所有生成的工具类 */
}

这是一个重大变化,因为 CSS 级联层有真正的优先级影响。低优先级层中的规则永远输给高优先级层中的规则,无论选择器特异性如何。这意味着:

css
@layer components {
  /* 特异性:0-1-0 */
  .card { padding: 1rem; }
}
 
@layer utilities {
  /* 特异性:0-1-0 — 相同特异性但胜出,因为 utilities 层在后面 */
  .p-4 { padding: 1rem; }
}

工具类永远覆盖组件。组件永远覆盖基础。这在概念上和 v3 中 Tailwind 的工作方式一样,但现在它是由浏览器的级联层机制强制执行的,而不是通过源码顺序操作。

添加自定义工具类#

在 v3 中,你通过插件 API 或 @layer utilities 定义自定义工具类:

js
// v3 — 插件方式
const plugin = require("tailwindcss/plugin");
 
module.exports = {
  plugins: [
    plugin(function ({ addUtilities }) {
      addUtilities({
        ".text-balance": {
          "text-wrap": "balance",
        },
        ".text-pretty": {
          "text-wrap": "pretty",
        },
      });
    }),
  ],
};

在 v4 中,自定义工具类用 @utility 指令定义:

css
@import "tailwindcss";
 
@utility text-balance {
  text-wrap: balance;
}
 
@utility text-pretty {
  text-wrap: pretty;
}

@utility 指令告诉 Tailwind"这是一个工具类——把它放在 utilities 层中,允许它和变体一起使用。"最后这点是关键。用 @utility 定义的工具类自动支持 hover:focus:md: 和所有其他变体:

html
<p class="text-pretty md:text-balance">...</p>

自定义变体#

你也可以用 @variant 定义自定义变体:

css
@import "tailwindcss";
 
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);
html
<button class="hocus:bg-brand-500 theme-dark:text-white">
  Click me
</button>

这替代了 v3 的 addVariant 插件 API 在大多数场景下的使用。它不那么强大(你无法做编程式的变体生成),但覆盖了人们实际使用的 90%。

容器查询:内置,无需插件#

容器查询是 v3 中最受请求的功能之一。你可以通过 @tailwindcss/container-queries 插件获得,但它是附加的。在 v4 中,它们内置于框架。

基本用法#

@container 标记一个容器,用 @ 前缀查询其大小:

html
<!-- 标记父元素为容器 -->
<div class="@container">
  <!-- 相对于父元素宽度响应,不是视口 -->
  <div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
    <div class="p-4">卡片 1</div>
    <div class="p-4">卡片 2</div>
    <div class="p-4">卡片 3</div>
  </div>
</div>

@md@lg 等变体的工作方式类似响应式断点,但是相对于最近的 @container 祖先而不是视口。断点值对应 Tailwind 的默认断点:

变体最小宽度
@sm24rem (384px)
@md28rem (448px)
@lg32rem (512px)
@xl36rem (576px)
@2xl42rem (672px)

命名容器#

你可以给容器命名以查询特定祖先:

html
<div class="@container/sidebar">
  <div class="@container/card">
    <!-- 查询 card 容器 -->
    <div class="@md/card:text-lg">...</div>
 
    <!-- 查询 sidebar 容器 -->
    <div class="@lg/sidebar:hidden">...</div>
  </div>
</div>

为什么这很重要#

容器查询改变了你对响应式设计的思考方式。不再是"在这个视口宽度下,显示三列",而是"当这个组件的容器足够宽时,显示三列"。组件变得真正自包含。你可以把一个卡片组件从全宽布局移到侧边栏,它会自动适应。不需要媒体查询的花式操作。

我一直在重构我的组件库,默认使用容器查询而不是视口断点。结果是组件放在任何地方都能工作,父组件不需要知道组件的响应式行为。

html
<!-- 这个组件适应任何放置它的容器 -->
<article class="@container">
  <div class="grid grid-cols-1 @md:grid-cols-[200px_1fr] gap-4">
    <img
      class="w-full @md:w-auto rounded-lg aspect-video @md:aspect-square object-cover"
      src="/post-image.jpg"
      alt=""
    />
    <div>
      <h2 class="text-lg @lg:text-xl font-semibold">文章标题</h2>
      <p class="mt-2 text-sm @md:text-base text-gray-600">
        文章摘要放这里……
      </p>
      <div class="mt-4 hidden @md:flex gap-2">
        <span class="text-xs bg-gray-100 px-2 py-1 rounded">标签</span>
      </div>
    </div>
  </div>
</article>

真正有用的新变体#

v4 添加了几个我一直在用的新变体。它们填补了真实的空白。

starting: 变体#

这映射到 CSS @starting-style,让你定义元素首次出现时的初始状态。这是不用 JavaScript 就能实现元素入场动画的缺失拼图:

html
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
  <p>这个对话框打开时会淡入</p>
</dialog>

starting: 变体生成 @starting-style 块内的 CSS:

css
/* Tailwind 生成的内容 */
@starting-style {
  dialog[open] {
    opacity: 0;
  }
}
 
dialog[open] {
  opacity: 1;
  transition: opacity 300ms;
}

这对对话框、弹出框、下拉菜单来说意义重大——任何需要入场动画的东西。以前,你需要用 JavaScript 在下一帧添加一个类,或者用 @keyframes。现在只需一个工具类。

not-* 变体#

取反。我们一直想要的东西:

html
<!-- 除了最后一个子元素外,每个都有边框 -->
<div class="divide-y">
  <div class="not-last:pb-4">项目 1</div>
  <div class="not-last:pb-4">项目 2</div>
  <div class="not-last:pb-4">项目 3</div>
</div>
 
<!-- 给所有非 disabled 的元素添加样式 -->
<input class="not-disabled:hover:border-brand-500" />
 
<!-- 取反 data 属性 -->
<div class="not-data-active:opacity-50">...</div>

nth-* 变体#

直接访问 nth-childnth-of-type

html
<ul>
  <li class="nth-1:font-bold">第一项——加粗</li>
  <li class="nth-even:bg-gray-50">偶数行——灰色背景</li>
  <li class="nth-odd:bg-white">奇数行——白色背景</li>
  <li class="nth-[3n+1]:text-brand-500">每三个加一——品牌色</li>
</ul>

方括号语法(nth-[3n+1])支持任何有效的 nth-child 表达式。这替代了我过去为表格条纹和网格模式写的大量自定义 CSS。

in-* 变体(父状态)#

这是 group-* 的反向。不是"当我的父元素(group)被悬停时,给我添加样式",而是"当我在一个匹配此状态的父元素内部时,给我添加样式":

html
<div class="in-data-active:bg-brand-50">
  当任何祖先有 data-active 时获得背景色
</div>

**: 深层通用变体#

给所有后代添加样式,不仅仅是直接子元素。这是受控的力量——谨慎使用,但对散文内容和 CMS 输出来说非常有价值:

html
<!-- 这个 div 内所有层级的段落 -->
<div class="**:data-highlight:bg-yellow-100">
  <section>
    <p data-highlight>这个会被高亮</p>
    <div>
      <p data-highlight>这个也是,嵌套更深</p>
    </div>
  </section>
</div>

破坏性变更:实际上什么崩了#

我直说。如果你有一个大型 v3 项目,迁移不是轻而易举的事。以下是我的项目中出问题的地方:

1. 配置格式#

你的 tailwind.config.js 不能直接使用。你需要:

  • 把它转换为 @theme CSS(推荐,适配新架构)
  • 使用兼容层 @config 指令(快速迁移路径)
css
/* 快速迁移——保留你的旧配置 */
@import "tailwindcss";
@config "../../tailwind.config.js";

这个 @config 桥梁能用,但它明确是一个迁移工具。建议是随着时间推移逐步迁移到 @theme

2. 移除了废弃的工具类#

一些在 v3 中被废弃的工具类已经消失:

/* v4 中已移除 */
bg-opacity-*     → 使用 bg-black/50(斜杠透明度语法)
text-opacity-*   → 使用 text-black/50
border-opacity-* → 使用 border-black/50
flex-shrink-*    → 使用 shrink-*
flex-grow-*      → 使用 grow-*
overflow-ellipsis → 使用 text-ellipsis
decoration-slice  → 使用 box-decoration-slice
decoration-clone  → 使用 box-decoration-clone

如果你在 v3 中已经使用了现代语法(斜杠透明度、shrink-*),那就没问题。否则,这些都是简单的查找替换。

3. 默认调色板变化#

默认调色板略有调整。如果你依赖 v3 中精确的颜色值(不是按名称而是实际的十六进制值),你可能会注意到视觉差异。命名颜色(blue-500gray-200)仍然存在,但一些十六进制值变了。

4. 内容检测#

v3 需要显式的 content 配置:

js
// v3
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
};

v4 使用自动内容检测。它扫描你的项目根目录并自动找到模板文件。这大多数情况下"直接能用",但如果你的项目结构不寻常(monorepo 中有项目根目录外的包、模板文件在意外位置),你可能需要显式配置源路径:

css
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";

5. 插件 API 变化#

如果你写过自定义插件,API 变了。addUtilitiesaddComponentsaddBaseaddVariant 函数通过兼容层仍然能用,但 v4 的惯用方式是 CSS 原生:

js
// v3 插件
plugin(function ({ addUtilities, theme }) {
  addUtilities({
    ".scrollbar-hide": {
      "-ms-overflow-style": "none",
      "scrollbar-width": "none",
      "&::-webkit-scrollbar": {
        display: "none",
      },
    },
  });
});
css
/* v4 — 纯 CSS */
@utility scrollbar-hide {
  -ms-overflow-style: none;
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

大多数官方插件(@tailwindcss/typography@tailwindcss/forms@tailwindcss/aspect-ratio)都有 v4 兼容版本。第三方插件就不一定了——迁移前检查它们的仓库。

6. JIT 是唯一模式#

在 v3 中,你可以选择不使用 JIT 模式(虽然几乎没人这样做)。在 v4 中,没有非 JIT 模式。一切都是按需生成的,始终如此。如果你有某种理由使用旧的 AOT(提前编译)引擎,那条路已经没了。

7. 一些变体语法变化#

少数变体被重命名或改变了行为:

html
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
 
<!-- v4 — >* 部分现在使用 inset 变体语法 -->
<div class="*:p-4">...</div>

任意变体语法 [&...] 仍然有效,但 v4 为常见模式提供了命名替代方案。

迁移指南:真实过程#

以下是我实际的迁移过程,不是文档中的快乐路径,而是真实的样子。

第一步:运行官方 codemod#

Tailwind 提供了一个 codemod 处理大部分机械性变更:

bash
npx @tailwindcss/upgrade

它自动做了很多事:

  • @tailwind 指令转换为 @import "tailwindcss"
  • 重命名废弃的工具类
  • 更新变体语法
  • 将透明度工具类转换为斜杠语法(bg-opacity-50bg-black/50
  • 从你的配置创建基础的 @theme

Codemod 处理得好的#

  • 工具类重命名(近乎完美)
  • 指令语法变化
  • 简单的主题值(颜色、间距、字体)
  • 透明度语法迁移

Codemod 处理不了的#

  • 复杂的插件转换
  • 动态配置值(JavaScript 中的 theme() 调用)
  • 条件主题配置(如基于环境的主题值)
  • 自定义插件 API 迁移
  • 新解析器解释方式不同的任意值边界情况
  • JavaScript 中动态构建的类名(模板字面量、字符串拼接)

第二步:修复 PostCSS 配置#

对大多数设置,你需要更新 PostCSS 配置:

js
// postcss.config.js — v4
module.exports = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

注意:插件名从 tailwindcss 改为 @tailwindcss/postcss。如果你使用 Vite,可以完全跳过 PostCSS,使用 Vite 插件:

js
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
 
export default defineConfig({
  plugins: [tailwindcss()],
});

第三步:转换主题配置#

这是手动部分。把你的 tailwind.config.js 主题值转换为 @theme

js
// v3 配置 — 迁移前
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          light: "#60a5fa",
          DEFAULT: "#3b82f6",
          dark: "#1d4ed8",
        },
      },
      fontSize: {
        "2xs": ["0.65rem", { lineHeight: "1rem" }],
      },
      animation: {
        "fade-in": "fade-in 0.5s ease-out",
      },
      keyframes: {
        "fade-in": {
          "0%": { opacity: "0" },
          "100%": { opacity: "1" },
        },
      },
    },
  },
};
css
/* v4 CSS — 迁移后 */
@import "tailwindcss";
 
@theme {
  --color-brand-light: #60a5fa;
  --color-brand: #3b82f6;
  --color-brand-dark: #1d4ed8;
 
  --text-2xs: 0.65rem;
  --text-2xs--line-height: 1rem;
 
  --animate-fade-in: fade-in 0.5s ease-out;
}
 
@keyframes fade-in {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

注意 keyframes 从 @theme 中移出来变成了普通 CSS @keyframes@theme 中的动画名只是引用它们。这更干净——keyframes 是 CSS,就应该写成 CSS。

第四步:视觉回归测试#

这不可商量。迁移后,我打开了应用的每个页面进行目视检查。我也运行了 Playwright 截图测试(如果你有的话)。Codemod 很好但不完美。我在目视检查中发现的问题:

  • 一些透明度语法迁移产生了略微不同结果的地方
  • 没有延续过来的自定义插件输出
  • 由于层排序导致的 z-index 堆叠变化
  • 一些 !important 覆盖在级联层下行为不同

第五步:更新第三方依赖#

检查每个 Tailwind 相关的包:

json
{
  "@tailwindcss/typography": "^1.0.0",
  "@tailwindcss/forms": "^1.0.0",
  "@tailwindcss/container-queries": "移除——现在内置了",
  "tailwindcss-animate": "检查 v4 支持",
  "prettier-plugin-tailwindcss": "更新到最新"
}

@tailwindcss/container-queries 插件不再需要——容器查询已内置。其他插件需要它们的 v4 兼容版本。

配合 Next.js 使用#

因为我大多数项目用 Next.js,这里是具体的设置。

PostCSS 方式(Next.js 推荐)#

Next.js 底层使用 PostCSS,所以 PostCSS 插件是自然的选择:

bash
npm install tailwindcss @tailwindcss/postcss
js
// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};
css
/* app/globals.css */
@import "tailwindcss";
 
@theme {
  --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}

这就是完整的设置。不需要 tailwind.config.js,不需要 autoprefixer(v4 内部处理浏览器前缀)。

CSS 导入顺序#

有一个坑让我踩了:由于级联层的存在,v4 中 CSS 导入顺序更重要了。你的 @import "tailwindcss" 应该在自定义样式之前:

css
/* 正确顺序 */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
 
/* 你的内联 @theme、@utility 等 */

如果你在 Tailwind 之前导入自定义 CSS,你的样式可能会在更低的级联层中,被意外覆盖。

深色模式#

深色模式概念上的工作方式不变,但配置移到了 CSS:

css
@import "tailwindcss";
 
/* 使用基于类的深色模式(默认是基于媒体查询的) */
@variant dark (&:where(.dark, .dark *));

这替代了 v3 配置:

js
// v3
module.exports = {
  darkMode: "class",
};

@variant 方式更灵活。你可以随心所欲地定义深色模式——基于类、基于 data 属性或基于媒体查询:

css
/* data 属性方式 */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
 
/* 媒体查询——这是默认的,所以你不需要声明 */
@variant dark (@media (prefers-color-scheme: dark));

Turbopack 兼容性#

如果你使用 Next.js 配合 Turbopack(现在是默认的开发打包器),v4 运行得很好。Rust 引擎和 Turbopack 自身基于 Rust 的架构配合得很好。我测量了开发启动时间:

设置v3 + Webpackv3 + Turbopackv4 + Turbopack
冷启动4.8s2.1s1.3s
HMR(CSS 变更)450ms180ms40ms

CSS 变更 40ms 的 HMR 几乎感知不到。感觉就是即时的。

性能深入:构建速度之外#

Oxide 引擎的好处不仅仅是原始构建速度。

内存使用#

v4 使用的内存显著减少。在我那个 847 组件的项目上:

指标v3v4
峰值内存(构建)380MB45MB
稳态(开发)210MB28MB

这对内存受限的 CI/CD 流水线很重要,对同时运行十个进程的开发机器也很重要。

CSS 输出大小#

v4 生成的 CSS 输出略小,因为新引擎在去重和死代码消除方面做得更好:

v3 输出:34.2 KB(gzip 后)
v4 输出:29.8 KB(gzip 后)

不改任何代码就减少了 13%。不是颠覆性的,但是免费的性能提升。

主题值的 Tree Shaking#

在 v4 中,如果你定义了一个主题值但从未在模板中使用它,对应的 CSS 自定义属性仍然会被输出(它在 @theme 中,映射到 :root 变量)。但是,未使用值的工具类不会被生成。这和 v3 的 JIT 行为相同,但值得注意:你的 CSS 自定义属性始终可用,即使没有工具类使用它们。

如果你想阻止某些主题值生成 CSS 自定义属性,可以使用 @theme inline

css
@theme inline {
  /* 这些值生成工具类但不生成 CSS 自定义属性 */
  --color-internal-debug: #ff00ff;
  --spacing-magic-number: 3.7rem;
}

这对你不想暴露为 CSS 变量的内部设计令牌很有用。

高级用法:多品牌主题组合#

v4 让多品牌主题显著变得更容易的一个模式。因为主题值是 CSS 自定义属性,你可以在运行时替换它们:

css
@import "tailwindcss";
 
@theme {
  --color-brand: var(--brand-primary, #3b82f6);
  --color-brand-light: var(--brand-light, #60a5fa);
  --color-brand-dark: var(--brand-dark, #1d4ed8);
}
 
/* 品牌覆盖 */
.theme-acme {
  --brand-primary: #e11d48;
  --brand-light: #fb7185;
  --brand-dark: #9f1239;
}
 
.theme-globex {
  --brand-primary: #059669;
  --brand-light: #34d399;
  --brand-dark: #047857;
}
html
<body class="theme-acme">
  <!-- 所有 bg-brand、text-brand 等都使用 Acme 的颜色 -->
  <div class="bg-brand text-white">Acme Corp</div>
</body>

在 v3 中,这需要一个自定义插件或在 Tailwind 外面做复杂的 CSS 变量设置。在 v4 中,这很自然——主题就是 CSS 变量,CSS 变量会级联。这就是 CSS 优先方式让人觉得正确的那种东西。

我怀念 v3 的什么#

让我客观一些。v3 有些东西我在 v4 中确实怀念:

1. 用于编程式主题的 JavaScript 配置。 我有个项目,在配置中用 JavaScript 函数从单一品牌色生成整个色阶。在 v4 中,你无法在 @theme 里这样做——你需要一个生成 CSS 文件的构建步骤,或者一次性计算好颜色然后粘贴进去。@config 兼容层有所帮助,但这不是长期方案。

2. 刚发布时 IntelliSense 更好。 v3 的 VS Code 扩展经过了多年打磨。v4 的 IntelliSense 能用,但早期有一些不足——自定义 @theme 值有时无法自动补全,@utility 定义也不总是被识别。最近的更新已经大幅改善了,但值得一提。

3. 生态成熟度。 v3 周围的生态系统非常庞大。Headless UI、Radix、shadcn/ui、Flowbite、DaisyUI——一切都是针对 v3 测试的。v4 支持正在陆续推出但还不是普遍的。我不得不给一个组件库提了 PR 来修复 v4 兼容性。

你应该迁移吗?#

以下是我在用了 v4 好几周后的决策框架:

现在就迁移,如果:#

  • 你在开始一个新项目(显而易见的选择——直接用 v4)
  • 你的项目几乎没有自定义插件
  • 你想在大型项目中获得性能优势
  • 你已经在使用现代 Tailwind 模式(斜杠透明度、shrink-* 等)
  • 你需要容器查询但不想额外装插件

等一等,如果:#

  • 你严重依赖尚未支持 v4 的第三方 Tailwind 插件
  • 你有复杂的编程式主题配置
  • 你的项目很稳定,不在积极开发中(何必动它?)
  • 你正在功能冲刺中(在冲刺间迁移,不要在冲刺中)

不要迁移,如果:#

  • 你还在 v2 或更早版本(先升级到 v3,稳定后再考虑 v4)
  • 你的项目几个月后就结束了(不值得折腾)

我的真实看法#

对于新项目,v4 是毫无疑问的选择。CSS 优先配置更干净,引擎快得多,新功能(容器查询、@starting-style、新变体)确实有用。

对于现有项目,我建议分阶段:

  1. 现在: 新项目全部用 v4 开始
  2. 很快: 尝试把一个小型内部项目转换到 v4
  3. 准备好后: 在一个安静的冲刺期间迁移生产项目,配合视觉回归测试

如果你有所准备,迁移并不痛苦。Codemod 处理了 80% 的工作。剩下的 20% 是手动但直接的。中型项目预留一天,大型项目两到三天。

Tailwind v4 是 Tailwind 本应有的样子。JavaScript 配置一直是对当时工具的妥协。CSS 优先配置、原生级联层、Rust 引擎——这些不是潮流,是框架在追赶平台。Web 平台变得更好了,Tailwind v4 选择顺应它而不是对抗它。

把你的设计令牌写在 CSS 中,用 CSS 特性组合它们,让浏览器自己的级联处理优先级——这是正确的方向。走了四个大版本才到这里,但结果是迄今为止最一致的 Tailwind 版本。

下个项目就用它吧。你不会想回头的。

相关文章