CSS 优先配置、@layer 集成、内置容器查询、新引擎性能、破坏性变更,以及我从 v3 迁移到 v4 的真实体验。
我从 v1.x 就开始用 Tailwind CSS 了,那时候社区一半人觉得它是个怪胎,另一半人已经靠它疯狂出活了。每个大版本都是一次重大飞跃,但 v4 不一样。它不仅仅是一个功能版本,而是一次从头开始的架构重写,改变了你和框架之间的根本契约。
把两个生产项目从 v3 迁移到 v4,又在 v4 上从零开始了三个新项目之后,我对什么是真正的改进、什么还粗糙、以及你今天是否应该迁移,有了清晰的判断。不吹不黑——只讲我观察到的。
Tailwind CSS v4 同时是三件事:
tailwind.config.js 成为默认方式@layer、容器查询、@starting-style 和级联层成为一等公民你到处看到的标题是"快 10 倍"。这是真的,但它低估了实际的变化。配置和扩展 Tailwind 的心智模型已经根本性地转变了。你现在是在用 CSS 工作,而不是用一个生成 CSS 的 JavaScript 配置对象。
以下是一个最简的 Tailwind v4 设置:
/* app.css — 这就是全部设置 */
@import "tailwindcss";就这样。没有配置文件。没有 PostCSS 插件配置(对大多数场景而言)。没有 @tailwind base; @tailwind components; @tailwind utilities; 指令。一行导入,就能跑。
对比 v3:
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};// v3 — postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};三个文件精简为一行。这不仅仅是少了样板代码——是减少了配置出错的可能性。在 v4 中,内容检测是自动的,它会自动扫描你的项目文件,无需你拼写 glob 模式。
这是最大的概念转变。在 v3 中,你通过 JavaScript 配置对象自定义 Tailwind:
// 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 里:
@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 运行的地方都能访问:
/* 这是免费获得的 */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* 任何地方都能用——不需要 Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. 你可以在 @theme 中使用 CSS 特性。 媒体查询、light-dark()、calc()——真正的 CSS 在这里能用,因为它本身就是真正的 CSS:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. 和你的其他 CSS 放在一起。 你的主题、自定义工具类和基础样式全都用同一种语言,想放同一个文件也行。不用在"CSS 世界"和"JavaScript 配置世界"之间来回切换。
在 v3 中你有 theme(替换)和 theme.extend(合并)。在 v4 中,心智模型不同:
@import "tailwindcss";
/* 这是扩展默认主题——在现有颜色旁边添加品牌色 */
@theme {
--color-brand-500: #3b82f6;
}如果你想完全替换一个命名空间(比如移除所有默认颜色),用 @theme 配合 --color-* 通配符重置:
@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; 然后不动间距就行。
对于更大的项目,你可以把主题拆分到多个文件:
/* 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;
}/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";这比 v3 那种把巨大的 tailwind.config.js 拆分或试图用 require() 的模式干净多了。
Tailwind v4 的引擎是用 Rust 完全重写的,他们叫它 Oxide。我对"快 10 倍"的说法持怀疑态度——营销数字很少经得起真实项目的检验。所以我做了基准测试。
我的测试项目: 一个 Next.js 应用,847 个组件,142 个页面,大约 23,000 处 Tailwind 类名使用。
| 指标 | v3 (Node) | v4 (Oxide) | 提升 |
|---|---|---|---|
| 首次构建 | 4,280ms | 387ms | 11x |
| 增量(编辑 1 个文件) | 340ms | 18ms | 19x |
| 全量重建(清理后) | 5,100ms | 510ms | 10x |
| 开发服务器启动 | 3,200ms | 290ms | 11x |
对我的项目来说,"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 当作一个构建步骤来思考了。它变得像语法高亮一样——永远在那里,从不碍事。在一整天的开发中,这种心理差异是很显著的。
在 v3 中,Tailwind 使用自己的层系统,有 @layer base、@layer components 和 @layer utilities。它们看起来像 CSS 级联层但不是——它们是 Tailwind 特有的指令,控制生成的 CSS 出现在输出中的位置。
在 v4 中,Tailwind 使用真正的 CSS 级联层:
/* v4 输出 — 简化版 */
@layer theme, base, components, utilities;
@layer base {
/* 重置,preflight */
}
@layer components {
/* 你的组件类 */
}
@layer utilities {
/* 所有生成的工具类 */
}这是一个重大变化,因为 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 定义自定义工具类:
// 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 指令定义:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}@utility 指令告诉 Tailwind"这是一个工具类——把它放在 utilities 层中,允许它和变体一起使用。"最后这点是关键。用 @utility 定义的工具类自动支持 hover:、focus:、md: 和所有其他变体:
<p class="text-pretty md:text-balance">...</p>你也可以用 @variant 定义自定义变体:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Click me
</button>这替代了 v3 的 addVariant 插件 API 在大多数场景下的使用。它不那么强大(你无法做编程式的变体生成),但覆盖了人们实际使用的 90%。
容器查询是 v3 中最受请求的功能之一。你可以通过 @tailwindcss/container-queries 插件获得,但它是附加的。在 v4 中,它们内置于框架。
用 @container 标记一个容器,用 @ 前缀查询其大小:
<!-- 标记父元素为容器 -->
<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 的默认断点:
| 变体 | 最小宽度 |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
你可以给容器命名以查询特定祖先:
<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>容器查询改变了你对响应式设计的思考方式。不再是"在这个视口宽度下,显示三列",而是"当这个组件的容器足够宽时,显示三列"。组件变得真正自包含。你可以把一个卡片组件从全宽布局移到侧边栏,它会自动适应。不需要媒体查询的花式操作。
我一直在重构我的组件库,默认使用容器查询而不是视口断点。结果是组件放在任何地方都能工作,父组件不需要知道组件的响应式行为。
<!-- 这个组件适应任何放置它的容器 -->
<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 就能实现元素入场动画的缺失拼图:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>这个对话框打开时会淡入</p>
</dialog>starting: 变体生成 @starting-style 块内的 CSS:
/* Tailwind 生成的内容 */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}这对对话框、弹出框、下拉菜单来说意义重大——任何需要入场动画的东西。以前,你需要用 JavaScript 在下一帧添加一个类,或者用 @keyframes。现在只需一个工具类。
not-* 变体#取反。我们一直想要的东西:
<!-- 除了最后一个子元素外,每个都有边框 -->
<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-child 和 nth-of-type:
<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)被悬停时,给我添加样式",而是"当我在一个匹配此状态的父元素内部时,给我添加样式":
<div class="in-data-active:bg-brand-50">
当任何祖先有 data-active 时获得背景色
</div>**: 深层通用变体#给所有后代添加样式,不仅仅是直接子元素。这是受控的力量——谨慎使用,但对散文内容和 CMS 输出来说非常有价值:
<!-- 这个 div 内所有层级的段落 -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>这个会被高亮</p>
<div>
<p data-highlight>这个也是,嵌套更深</p>
</div>
</section>
</div>我直说。如果你有一个大型 v3 项目,迁移不是轻而易举的事。以下是我的项目中出问题的地方:
你的 tailwind.config.js 不能直接使用。你需要:
@theme CSS(推荐,适配新架构)@config 指令(快速迁移路径)/* 快速迁移——保留你的旧配置 */
@import "tailwindcss";
@config "../../tailwind.config.js";这个 @config 桥梁能用,但它明确是一个迁移工具。建议是随着时间推移逐步迁移到 @theme。
一些在 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-*),那就没问题。否则,这些都是简单的查找替换。
默认调色板略有调整。如果你依赖 v3 中精确的颜色值(不是按名称而是实际的十六进制值),你可能会注意到视觉差异。命名颜色(blue-500、gray-200)仍然存在,但一些十六进制值变了。
v3 需要显式的 content 配置:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 使用自动内容检测。它扫描你的项目根目录并自动找到模板文件。这大多数情况下"直接能用",但如果你的项目结构不寻常(monorepo 中有项目根目录外的包、模板文件在意外位置),你可能需要显式配置源路径:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";如果你写过自定义插件,API 变了。addUtilities、addComponents、addBase 和 addVariant 函数通过兼容层仍然能用,但 v4 的惯用方式是 CSS 原生:
// v3 插件
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — 纯 CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}大多数官方插件(@tailwindcss/typography、@tailwindcss/forms、@tailwindcss/aspect-ratio)都有 v4 兼容版本。第三方插件就不一定了——迁移前检查它们的仓库。
在 v3 中,你可以选择不使用 JIT 模式(虽然几乎没人这样做)。在 v4 中,没有非 JIT 模式。一切都是按需生成的,始终如此。如果你有某种理由使用旧的 AOT(提前编译)引擎,那条路已经没了。
少数变体被重命名或改变了行为:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — >* 部分现在使用 inset 变体语法 -->
<div class="*:p-4">...</div>任意变体语法 [&...] 仍然有效,但 v4 为常见模式提供了命名替代方案。
以下是我实际的迁移过程,不是文档中的快乐路径,而是真实的样子。
Tailwind 提供了一个 codemod 处理大部分机械性变更:
npx @tailwindcss/upgrade它自动做了很多事:
@tailwind 指令转换为 @import "tailwindcss"bg-opacity-50 到 bg-black/50)@theme 块theme() 调用)对大多数设置,你需要更新 PostCSS 配置:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};注意:插件名从 tailwindcss 改为 @tailwindcss/postcss。如果你使用 Vite,可以完全跳过 PostCSS,使用 Vite 插件:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});这是手动部分。把你的 tailwind.config.js 主题值转换为 @theme:
// 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" },
},
},
},
},
};/* 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 很好但不完美。我在目视检查中发现的问题:
!important 覆盖在级联层下行为不同检查每个 Tailwind 相关的包:
{
"@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,所以 PostCSS 插件是自然的选择:
npm install tailwindcss @tailwindcss/postcss// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};/* 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 内部处理浏览器前缀)。
有一个坑让我踩了:由于级联层的存在,v4 中 CSS 导入顺序更重要了。你的 @import "tailwindcss" 应该在自定义样式之前:
/* 正确顺序 */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* 你的内联 @theme、@utility 等 */如果你在 Tailwind 之前导入自定义 CSS,你的样式可能会在更低的级联层中,被意外覆盖。
深色模式概念上的工作方式不变,但配置移到了 CSS:
@import "tailwindcss";
/* 使用基于类的深色模式(默认是基于媒体查询的) */
@variant dark (&:where(.dark, .dark *));这替代了 v3 配置:
// v3
module.exports = {
darkMode: "class",
};@variant 方式更灵活。你可以随心所欲地定义深色模式——基于类、基于 data 属性或基于媒体查询:
/* data 属性方式 */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* 媒体查询——这是默认的,所以你不需要声明 */
@variant dark (@media (prefers-color-scheme: dark));如果你使用 Next.js 配合 Turbopack(现在是默认的开发打包器),v4 运行得很好。Rust 引擎和 Turbopack 自身基于 Rust 的架构配合得很好。我测量了开发启动时间:
| 设置 | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| 冷启动 | 4.8s | 2.1s | 1.3s |
| HMR(CSS 变更) | 450ms | 180ms | 40ms |
CSS 变更 40ms 的 HMR 几乎感知不到。感觉就是即时的。
Oxide 引擎的好处不仅仅是原始构建速度。
v4 使用的内存显著减少。在我那个 847 组件的项目上:
| 指标 | v3 | v4 |
|---|---|---|
| 峰值内存(构建) | 380MB | 45MB |
| 稳态(开发) | 210MB | 28MB |
这对内存受限的 CI/CD 流水线很重要,对同时运行十个进程的开发机器也很重要。
v4 生成的 CSS 输出略小,因为新引擎在去重和死代码消除方面做得更好:
v3 输出:34.2 KB(gzip 后)
v4 输出:29.8 KB(gzip 后)
不改任何代码就减少了 13%。不是颠覆性的,但是免费的性能提升。
在 v4 中,如果你定义了一个主题值但从未在模板中使用它,对应的 CSS 自定义属性仍然会被输出(它在 @theme 中,映射到 :root 变量)。但是,未使用值的工具类不会被生成。这和 v3 的 JIT 行为相同,但值得注意:你的 CSS 自定义属性始终可用,即使没有工具类使用它们。
如果你想阻止某些主题值生成 CSS 自定义属性,可以使用 @theme inline:
@theme inline {
/* 这些值生成工具类但不生成 CSS 自定义属性 */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}这对你不想暴露为 CSS 变量的内部设计令牌很有用。
v4 让多品牌主题显著变得更容易的一个模式。因为主题值是 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;
}<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 有些东西我在 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 好几周后的决策框架:
shrink-* 等)对于新项目,v4 是毫无疑问的选择。CSS 优先配置更干净,引擎快得多,新功能(容器查询、@starting-style、新变体)确实有用。
对于现有项目,我建议分阶段:
如果你有所准备,迁移并不痛苦。Codemod 处理了 80% 的工作。剩下的 20% 是手动但直接的。中型项目预留一天,大型项目两到三天。
Tailwind v4 是 Tailwind 本应有的样子。JavaScript 配置一直是对当时工具的妥协。CSS 优先配置、原生级联层、Rust 引擎——这些不是潮流,是框架在追赶平台。Web 平台变得更好了,Tailwind v4 选择顺应它而不是对抗它。
把你的设计令牌写在 CSS 中,用 CSS 特性组合它们,让浏览器自己的级联处理优先级——这是正确的方向。走了四个大版本才到这里,但结果是迄今为止最一致的 Tailwind 版本。
下个项目就用它吧。你不会想回头的。