Tailwind CSS v4: Những Gì Thực Sự Thay Đổi và Có Nên Migrate Không
Cấu hình CSS-first, tích hợp @layer, container queries tích hợp sẵn, hiệu năng engine mới, breaking changes, và trải nghiệm migrate thực tế từ v3 lên v4.
Tôi đã dùng Tailwind CSS từ v1.x, khi một nửa cộng đồng nghĩ nó là thảm họa và nửa còn lại không ngừng ship sản phẩm với nó. Mỗi phiên bản lớn đều là một bước nhảy đáng kể, nhưng v4 khác biệt. Đây không chỉ là bản cập nhật tính năng. Đây là viết lại kiến trúc từ gốc, thay đổi hợp đồng cơ bản giữa bạn và framework.
Sau khi migrate hai dự án production từ v3 sang v4 và bắt đầu ba dự án mới trên v4 từ đầu, tôi có bức tranh rõ ràng về những gì thực sự tốt hơn, những gì còn thô, và liệu bạn có nên migrate hôm nay hay không. Không hype, không phẫn nộ — chỉ những gì tôi quan sát được.
Bức Tranh Toàn Cảnh: v4 Thực Sự Là Gì#
Tailwind CSS v4 là ba thứ cùng lúc:
- Engine mới — viết lại từ JavaScript sang Rust (engine Oxide), làm build nhanh hơn đáng kể
- Mô hình cấu hình mới — cấu hình CSS-first thay thế
tailwind.config.jslàm mặc định - Tích hợp chặt chẽ hơn với nền tảng CSS — native
@layer, container queries,@starting-style, và cascade layers là công dân hạng nhất
Tiêu đề bạn sẽ thấy khắp nơi là "nhanh hơn 10x." Con số đó thật, nhưng nó chưa nói hết sự thay đổi thực sự. Mô hình tư duy để cấu hình và mở rộng Tailwind đã thay đổi căn bản. Bạn đang làm việc với CSS bây giờ, không phải đối tượng cấu hình JavaScript tạo ra CSS.
Đây là giao diện Tailwind v4 tối giản:
/* app.css — đây là toàn bộ setup */
@import "tailwindcss";Thế thôi. Không file config. Không cấu hình PostCSS plugin (cho hầu hết setup). Không có directive @tailwind base; @tailwind components; @tailwind utilities;. Một import, và bạn chạy được.
So sánh với 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: {},
},
};Ba file giảm xuống một dòng. Đó không chỉ là ít boilerplate — mà là ít bề mặt cho cấu hình sai. Trong v4, phát hiện content là tự động. Nó quét file dự án mà không cần bạn viết ra glob pattern.
Cấu Hình CSS-First Với @theme#
Đây là sự thay đổi khái niệm lớn nhất. Trong v3, bạn tùy chỉnh Tailwind qua đối tượng config JavaScript:
// 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",
},
},
},
};Trong v4, tất cả nằm trong CSS sử dụng directive @theme:
@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;
}Ban đầu, tôi kháng cự điều này. Tôi thích có một đối tượng JavaScript duy nhất để thấy toàn bộ design system. Nhưng sau một tuần với cách tiếp cận CSS, tôi đổi ý vì ba lý do:
1. CSS custom properties native được expose tự động. Mỗi giá trị bạn định nghĩa trong @theme trở thành CSS custom property trên :root. Nghĩa là giá trị theme của bạn có thể truy cập trong CSS thuần, trong CSS Modules, trong thẻ <style>, bất cứ đâu CSS chạy:
/* bạn nhận được miễn phí */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* dùng ở bất đâu — không cần Tailwind */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Bạn có thể dùng tính năng CSS bên trong @theme. Media queries, light-dark(), calc() — CSS thật hoạt động ở đây vì nó là CSS thật:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Cùng vị trí với CSS khác. Theme, custom utility, và base style đều sống trong cùng ngôn ngữ, cùng file nếu bạn muốn. Không có chuyển ngữ cảnh giữa "thế giới CSS" và "thế giới config JavaScript."
Override vs Extend Theme Mặc Định#
Trong v3 bạn có theme (thay thế) vs theme.extend (merge). Trong v4, mô hình tư duy khác:
@import "tailwindcss";
/* Đây là EXTEND theme mặc định — thêm brand colors bên cạnh các màu có sẵn */
@theme {
--color-brand-500: #3b82f6;
}Nếu bạn muốn thay thế hoàn toàn một namespace (như xóa tất cả màu mặc định), dùng @theme với wildcard reset --color-*:
@import "tailwindcss";
@theme {
/* Xóa tất cả màu mặc định trước */
--color-*: initial;
/* Giờ chỉ định nghĩa màu của bạn */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Pattern wildcard reset này thanh lịch. Bạn chọn chính xác phần nào của theme mặc định giữ lại và phần nào thay thế. Muốn giữ tất cả spacing mặc định nhưng dùng color tùy chỉnh? Reset --color-*: initial; và để spacing nguyên.
Nhiều File Theme#
Cho dự án lớn hơn, bạn có thể tách theme ra nhiều file:
/* 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";Gọn gàng hơn nhiều so với pattern v3 có tailwind.config.js khổng lồ hoặc cố tách nó bằng require().
Engine Oxide: Thực Sự Nhanh Hơn 10x#
Engine của Tailwind v4 được viết lại hoàn toàn bằng Rust. Họ gọi nó là Oxide. Tôi hoài nghi về tuyên bố "nhanh hơn 10x" — con số marketing hiếm khi sống sót khi đối mặt với dự án thực. Nên tôi đã benchmark.
Dự án test: Ứng dụng Next.js với 847 component, 142 trang, khoảng 23.000 lần sử dụng class Tailwind.
| Metric | v3 (Node) | v4 (Oxide) | Cải thiện |
|---|---|---|---|
| Build ban đầu | 4.280ms | 387ms | 11x |
| Incremental (sửa 1 file) | 340ms | 18ms | 19x |
| Full rebuild (sạch) | 5.100ms | 510ms | 10x |
| Khởi động dev server | 3.200ms | 290ms | 11x |
Tuyên bố "10x" là bảo thủ cho dự án của tôi. Incremental build là nơi nó thực sự tỏa sáng — 18ms nghĩa là gần như tức thì. Bạn lưu file và trình duyệt có style mới trước khi bạn kịp chuyển tab.
Tại Sao Nhanh Hơn Nhiều Như Vậy?#
Ba lý do:
1. Rust thay vì JavaScript. CSS parser core, phát hiện class, và tạo code đều là Rust native. Đây không phải "viết lại bằng Rust cho vui" — CSS parsing thực sự là công việc CPU-bound nơi code native có lợi thế lớn so với V8.
2. Không có PostCSS trong đường dẫn nóng. Trong v3, Tailwind là PostCSS plugin. Mỗi build nghĩa là: parse CSS thành PostCSS AST, chạy plugin Tailwind, serialize lại thành CSS string, rồi các PostCSS plugin khác chạy. Trong v4, Tailwind có CSS parser riêng đi thẳng từ source sang output. PostCSS vẫn được hỗ trợ cho tương thích, nhưng đường dẫn chính bỏ qua nó hoàn toàn.
3. Xử lý incremental thông minh hơn. Engine mới cache tích cực. Khi bạn sửa một file, nó chỉ re-scan file đó tìm class name và chỉ tạo lại CSS rule đã thay đổi. Engine v3 thông minh hơn người ta nghĩ về điều này (JIT mode đã là incremental), nhưng v4 đi xa hơn nhiều với fine-grained dependency tracking.
Tốc Độ Có Thực Sự Quan Trọng?#
Có, nhưng không phải vì lý do bạn nghĩ. Với hầu hết dự án, tốc độ build của v3 "ổn." Bạn chờ vài trăm mili giây trong dev. Không đau.
Tốc độ v4 quan trọng vì nó làm Tailwind trở nên vô hình trong toolchain. Khi build dưới 20ms, bạn ngừng nghĩ Tailwind là một build step. Nó trở nên như syntax highlighting — luôn ở đó, không bao giờ cản trở. Sự khác biệt tâm lý đó đáng kể qua cả ngày phát triển.
Tích Hợp Native @layer#
Trong v3, Tailwind dùng hệ thống layer riêng với @layer base, @layer components, và @layer utilities. Chúng trông giống CSS cascade layers nhưng không phải — chúng là directive riêng của Tailwind điều khiển vị trí CSS sinh ra trong output.
Trong v4, Tailwind dùng CSS cascade layers thật:
/* v4 output — đơn giản hóa */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* class component của bạn */
}
@layer utilities {
/* tất cả utility class được sinh ra */
}Đây là thay đổi đáng kể vì CSS cascade layers có hàm ý specificity thực sự. Rule trong layer ưu tiên thấp hơn luôn thua rule trong layer ưu tiên cao hơn, bất kể specificity của selector. Nghĩa là:
@layer components {
/* specificity: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* specificity: 0-1-0 — cùng specificity nhưng thắng vì utilities layer đến sau */
.p-4 { padding: 1rem; }
}Utilities luôn override components. Components luôn override base. Đây là cách Tailwind hoạt động về mặt khái niệm trong v3, nhưng giờ nó được thực thi bởi cơ chế cascade layer của trình duyệt, không phải bằng thao tác thứ tự source.
Thêm Custom Utility#
Trong v3, bạn định nghĩa custom utility với plugin API hoặc @layer utilities:
// v3 — cách tiếp cận plugin
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};Trong v4, custom utility được định nghĩa với directive @utility:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}Directive @utility nói với Tailwind "đây là utility class — đặt vào utilities layer và cho phép dùng với variant." Phần cuối là quan trọng. Utility được định nghĩa bằng @utility tự động hoạt động với hover:, focus:, md:, và mọi variant khác:
<p class="text-pretty md:text-balance">...</p>Custom Variant#
Bạn cũng có thể định nghĩa custom variant với @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Click me
</button>Đây thay thế plugin API addVariant của v3 cho hầu hết use case. Nó kém mạnh hơn (bạn không thể tạo variant lập trình), nhưng cover 90% những gì người ta thực sự làm.
Container Queries: Tích Hợp Sẵn, Không Cần Plugin#
Container queries là một trong những tính năng được yêu cầu nhiều nhất trong v3. Bạn có thể có chúng với plugin @tailwindcss/container-queries, nhưng đó là add-on. Trong v4, chúng tích hợp sẵn trong framework.
Sử Dụng Cơ Bản#
Đánh dấu container với @container và query kích thước với prefix @:
<!-- đánh dấu parent là container -->
<div class="@container">
<!-- responsive theo chiều rộng parent, không phải viewport -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Card 1</div>
<div class="p-4">Card 2</div>
<div class="p-4">Card 3</div>
</div>
</div>Các variant @md, @lg, v.v. hoạt động giống responsive breakpoint nhưng tương đối với tổ tiên @container gần nhất thay vì viewport. Giá trị breakpoint tương ứng với breakpoint mặc định của Tailwind:
| Variant | Min-width |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Container Có Tên#
Bạn có thể đặt tên container để query tổ tiên cụ thể:
<div class="@container/sidebar">
<div class="@container/card">
<!-- query card container -->
<div class="@md/card:text-lg">...</div>
<!-- query sidebar container -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Tại Sao Điều Này Quan Trọng#
Container queries thay đổi cách bạn nghĩ về responsive design. Thay vì "ở chiều rộng viewport này, hiện ba cột," bạn nói "khi container của component đủ rộng, hiện ba cột." Component trở nên thực sự tự đủ. Bạn có thể di chuyển card component từ layout full-width sang sidebar và nó tự động thích ứng. Không cần thể dục media query.
Tôi đang refactor các component library để dùng container queries mặc định thay vì viewport breakpoint. Kết quả là component hoạt động ở bất đâu bạn đặt, mà parent không cần biết gì về responsive behavior của component.
<!-- Component này thích ứng với BẤT KỲ container nào nó được đặt vào -->
<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">Post Title</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
Post excerpt goes here...
</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">Tag</span>
</div>
</div>
</div>
</article>Variant Mới Thực Sự Hữu Ích#
v4 thêm nhiều variant mới mà tôi liên tục sử dụng. Chúng lấp đầy khoảng trống thực sự.
Variant starting:#
Variant này map tới CSS @starting-style, cho phép bạn định nghĩa trạng thái ban đầu của element khi nó xuất hiện lần đầu. Đây là mảnh ghép còn thiếu cho animation element entry mà không cần JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Dialog này fade in khi mở</p>
</dialog>Variant starting: sinh CSS bên trong block @starting-style:
/* Tailwind sinh ra */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Đây là bước tiến lớn cho dialog, popover, dropdown menu — bất cứ thứ gì cần animation khi xuất hiện. Trước đây, bạn cần JavaScript để thêm class ở frame tiếp theo, hoặc dùng @keyframes. Giờ nó chỉ là utility class.
Variant not-*#
Phủ định. Thứ chúng ta muốn từ lâu:
<!-- mỗi child trừ cái cuối có border -->
<div class="divide-y">
<div class="not-last:pb-4">Item 1</div>
<div class="not-last:pb-4">Item 2</div>
<div class="not-last:pb-4">Item 3</div>
</div>
<!-- style mọi thứ không bị disabled -->
<input class="not-disabled:hover:border-brand-500" />
<!-- phủ định data attribute -->
<div class="not-data-active:opacity-50">...</div>Variant nth-*#
Truy cập nth-child và nth-of-type trực tiếp:
<ul>
<li class="nth-1:font-bold">Item đầu tiên — bold</li>
<li class="nth-even:bg-gray-50">Hàng chẵn — bg xám</li>
<li class="nth-odd:bg-white">Hàng lẻ — bg trắng</li>
<li class="nth-[3n+1]:text-brand-500">Mỗi thứ ba+1 — brand color</li>
</ul>Cú pháp bracket (nth-[3n+1]) hỗ trợ bất kỳ biểu thức nth-child hợp lệ. Điều này thay thế rất nhiều CSS tùy chỉnh tôi từng viết cho table striping và grid pattern.
Variant in-* (Trạng Thái Parent)#
Đây là ngược lại của group-*. Thay vì "khi parent (group) được hover, style tôi," nó là "khi tôi ở trong parent match trạng thái này, style tôi":
<div class="in-data-active:bg-brand-50">
Div này có background khi bất kỳ tổ tiên nào có data-active
</div>Variant **: Deep Universal#
Style tất cả con cháu, không chỉ con trực tiếp. Đây là sức mạnh có kiểm soát — dùng thận trọng, nhưng vô giá cho nội dung prose và CMS output:
<!-- tất cả paragraph trong div này, ở bất kỳ độ sâu nào -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Cái này được highlight</p>
<div>
<p data-highlight>Cái này cũng vậy, lồng sâu hơn</p>
</div>
</section>
</div>Breaking Changes: Những Gì Thực Sự Hỏng#
Nói thẳng. Nếu bạn có dự án v3 lớn, migration không đơn giản. Đây là những gì hỏng trong dự án của tôi:
1. Định Dạng Cấu Hình#
tailwind.config.js không hoạt động ngay. Bạn cần:
- Chuyển sang
@themeCSS (khuyến nghị cho kiến trúc mới) - Dùng compatibility layer directive
@config(đường migrate nhanh)
/* migrate nhanh — giữ config cũ */
@import "tailwindcss";
@config "../../tailwind.config.js";Bridge @config này hoạt động, nhưng rõ ràng là công cụ migration. Khuyến nghị là chuyển sang @theme dần dần.
2. Đã Xóa Utility Deprecated#
Một số utility deprecated trong v3 đã bị xóa:
/* ĐÃ XÓA trong v4 */
bg-opacity-* → dùng bg-black/50 (cú pháp slash opacity)
text-opacity-* → dùng text-black/50
border-opacity-* → dùng border-black/50
flex-shrink-* → dùng shrink-*
flex-grow-* → dùng grow-*
overflow-ellipsis → dùng text-ellipsis
decoration-slice → dùng box-decoration-slice
decoration-clone → dùng box-decoration-clone
Nếu bạn đã dùng cú pháp hiện đại trong v3 (slash opacity, shrink-*), không vấn đề gì. Nếu không, đây là thay đổi find-and-replace đơn giản.
3. Thay Đổi Bảng Màu Mặc Định#
Bảng màu mặc định dịch chuyển nhẹ. Nếu bạn phụ thuộc vào giá trị màu chính xác từ v3 (không phải theo tên mà theo hex thực), bạn có thể thấy khác biệt thị giác. Các màu đặt tên (blue-500, gray-200) vẫn tồn tại nhưng một số giá trị hex đã thay đổi.
4. Phát Hiện Content#
v3 yêu cầu cấu hình content rõ ràng:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 dùng phát hiện content tự động. Nó quét project root và tìm file template tự động. Điều này hầu hết "just works," nhưng nếu bạn có cấu trúc dự án bất thường (monorepo với package ngoài project root, file template ở vị trí không mong đợi), bạn có thể cần cấu hình đường dẫn source rõ ràng:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Thay Đổi Plugin API#
Nếu bạn viết custom plugin, API đã thay đổi. Các hàm addUtilities, addComponents, addBase, và addVariant vẫn hoạt động qua compatibility layer, nhưng cách v4 idiomatic là CSS-native:
// v3 plugin
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — chỉ CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}Hầu hết plugin first-party (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) có phiên bản tương thích v4. Plugin bên thứ ba thì còn tùy — kiểm tra repo trước khi migrate.
6. JIT Là Chế Độ Duy Nhất#
Trong v3, bạn có thể tắt JIT mode (dù gần như không ai làm). Trong v4, không có chế độ non-JIT. Mọi thứ đều sinh on-demand, luôn luôn. Nếu bạn có lý do dùng engine AOT (ahead-of-time) cũ, con đường đó đã mất.
7. Một Số Thay Đổi Cú Pháp Variant#
Vài variant bị đổi tên hoặc thay đổi behavior:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — phần >* giờ dùng cú pháp inset variant -->
<div class="*:p-4">...</div>Cú pháp arbitrary variant [&...] vẫn hoạt động, nhưng v4 cung cấp alternative đặt tên cho các pattern phổ biến.
Hướng Dẫn Migration: Quy Trình Thực Tế#
Đây là cách tôi thực sự migrate, không phải happy path từ docs mà quy trình thực tế trông như thế nào.
Bước 1: Chạy Codemod Chính Thức#
Tailwind cung cấp codemod xử lý hầu hết thay đổi cơ học:
npx @tailwindcss/upgradeNó tự động xử lý nhiều thứ:
- Chuyển directive
@tailwindthành@import "tailwindcss" - Đổi tên utility class deprecated
- Cập nhật cú pháp variant
- Chuyển opacity utility sang cú pháp slash (
bg-opacity-50thànhbg-black/50) - Tạo block
@themecơ bản từ config
Codemod Xử Lý Tốt#
- Đổi tên utility class (gần hoàn hảo)
- Thay đổi cú pháp directive
- Giá trị theme đơn giản (colors, spacing, fonts)
- Migration cú pháp opacity
Codemod KHÔNG Xử Lý#
- Chuyển đổi plugin phức tạp
- Giá trị config động (lời gọi
theme()trong JavaScript) - Cấu hình theme có điều kiện (ví dụ giá trị theme dựa trên environment)
- Migration custom plugin API
- Edge case arbitrary value khi parser mới hiểu khác
- Class name được xây dựng động trong JavaScript (template literal, nối chuỗi)
Bước 2: Sửa Cấu Hình PostCSS#
Với hầu hết setup, bạn cập nhật PostCSS config:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Lưu ý: tên plugin thay đổi từ tailwindcss sang @tailwindcss/postcss. Nếu dùng Vite, bạn có thể bỏ PostCSS hoàn toàn và dùng Vite plugin:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Bước 3: Chuyển Đổi Cấu Hình Theme#
Đây là phần thủ công. Lấy giá trị theme từ tailwind.config.js và chuyển sang @theme:
// v3 config — trước
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 — sau */
@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; }
}Lưu ý keyframes chuyển ra khỏi @theme và trở thành CSS @keyframes bình thường. Tên animation trong @theme chỉ tham chiếu chúng. Gọn hơn — keyframes là CSS, chúng nên được viết bằng CSS.
Bước 4: Visual Regression Testing#
Điều này không thương lượng được. Sau migration, tôi mở mọi trang của app và kiểm tra trực quan. Tôi cũng chạy Playwright screenshot test (nếu có). Codemod tốt nhưng không hoàn hảo. Những thứ tôi phát hiện khi review trực quan:
- Vài nơi migration cú pháp opacity cho kết quả hơi khác
- Output của custom plugin không mang theo
- Thay đổi z-index stacking do thứ tự layer
- Một số
!importantoverride behave khác với cascade layers
Bước 5: Cập Nhật Dependency Bên Thứ Ba#
Kiểm tra mọi package liên quan Tailwind:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "XÓA — tích hợp sẵn rồi",
"tailwindcss-animate": "kiểm tra hỗ trợ v4",
"prettier-plugin-tailwindcss": "cập nhật bản mới nhất"
}Plugin @tailwindcss/container-queries không cần nữa — container queries đã tích hợp sẵn. Các plugin khác cần phiên bản tương thích v4.
Làm Việc Với Next.js#
Vì tôi dùng Next.js cho hầu hết dự án, đây là setup cụ thể.
Cách PostCSS (Khuyến Nghị Cho Next.js)#
Next.js dùng PostCSS bên dưới, nên PostCSS plugin là phù hợp tự nhiên:
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;
}Đó là toàn bộ setup. Không tailwind.config.js, không autoprefixer (v4 xử lý vendor prefix nội bộ).
Thứ Tự Import CSS#
Một thứ làm tôi vấp: thứ tự import CSS quan trọng hơn trong v4 vì cascade layers. @import "tailwindcss" phải đến trước custom styles:
/* thứ tự đúng */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* @theme, @utility inline, v.v. */Nếu bạn import custom CSS trước Tailwind, style có thể nằm trong cascade layer thấp hơn và bị override bất ngờ.
Dark Mode#
Dark mode hoạt động giống về khái niệm nhưng cấu hình chuyển sang CSS:
@import "tailwindcss";
/* Dùng dark mode dựa trên class (mặc định là dựa trên media) */
@variant dark (&:where(.dark, .dark *));Thay thế config v3:
// v3
module.exports = {
darkMode: "class",
};Cách @variant linh hoạt hơn. Bạn có thể định nghĩa dark mode tùy ý — dựa trên class, data-attribute, hoặc media-query:
/* cách tiếp cận data attribute */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — đây là mặc định, không cần khai báo */
@variant dark (@media (prefers-color-scheme: dark));Tương Thích Turbopack#
Nếu bạn dùng Next.js với Turbopack (giờ là dev bundler mặc định), v4 hoạt động tuyệt vời. Engine Rust mesh tốt với kiến trúc dựa Rust của chính Turbopack. Tôi đo thời gian khởi động dev:
| Setup | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Cold start | 4.8s | 2.1s | 1.3s |
| HMR (thay đổi CSS) | 450ms | 180ms | 40ms |
40ms HMR cho thay đổi CSS gần như không nhận ra. Cảm giác tức thì.
Phân Tích Hiệu Năng Sâu: Không Chỉ Tốc Độ Build#
Lợi ích của engine Oxide vượt xa tốc độ build.
Sử Dụng Bộ Nhớ#
v4 dùng ít bộ nhớ hơn đáng kể. Trên dự án 847-component của tôi:
| Metric | v3 | v4 |
|---|---|---|
| Peak memory (build) | 380MB | 45MB |
| Steady-state (dev) | 210MB | 28MB |
Điều này quan trọng cho CI/CD pipeline nơi bộ nhớ bị giới hạn, và cho máy phát triển chạy đồng thời mười process.
Kích Thước CSS Output#
v4 sinh CSS output nhỏ hơn chút vì engine mới tốt hơn ở deduplication và dead code elimination:
v3 output: 34.2 KB (gzipped)
v4 output: 29.8 KB (gzipped)
Giảm 13% mà không thay đổi code. Không biến đổi lớn, nhưng hiệu năng miễn phí.
Tree Shaking Giá Trị Theme#
Trong v4, nếu bạn định nghĩa giá trị theme nhưng không bao giờ dùng trong template, CSS custom property tương ứng vẫn được emit (nó trong @theme, map sang biến :root). Tuy nhiên, utility class cho giá trị không dùng không được sinh ra. Điều này giống behavior JIT của v3 nhưng đáng lưu ý: CSS custom properties luôn khả dụng, kể cả cho giá trị không có utility usage.
Nếu muốn ngăn một số giá trị theme sinh CSS custom properties, dùng @theme inline:
@theme inline {
/* Giá trị này sinh utility nhưng KHÔNG sinh CSS custom properties */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Hữu ích cho design token nội bộ mà bạn không muốn expose thành biến CSS.
Nâng Cao: Kết Hợp Theme Cho Multi-Brand#
Một pattern mà v4 làm dễ hơn đáng kể là theming multi-brand. Vì giá trị theme là CSS custom properties, bạn có thể swap chúng tại runtime:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Brand override */
.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">
<!-- tất cả bg-brand, text-brand, v.v. dùng màu Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>Trong v3, điều này yêu cầu custom plugin hoặc setup CSS variable phức tạp ngoài Tailwind. Trong v4, nó tự nhiên — theme là biến CSS, và biến CSS cascade. Đây là loại thứ khiến cách tiếp cận CSS-first cảm thấy đúng.
Những Gì Tôi Nhớ Từ v3#
Công bằng mà nói. Có những thứ v3 làm mà tôi thực sự nhớ trong v4:
1. JavaScript config cho theme lập trình. Tôi có dự án nơi chúng tôi sinh color scale từ một brand color duy nhất bằng hàm JavaScript trong config. Trong v4, bạn không thể làm điều đó trong @theme — bạn cần build step sinh file CSS, hoặc tính toán màu một lần rồi paste vào. Compatibility layer @config giúp được, nhưng không phải câu chuyện dài hạn.
2. IntelliSense tốt hơn khi launch. Extension VS Code cho v3 được polish nhiều năm. IntelliSense v4 hoạt động nhưng có khoảng trống ban đầu — giá trị @theme tùy chỉnh đôi khi không autocomplete, và định nghĩa @utility không phải lúc nào cũng được nhận. Điều này đã cải thiện đáng kể với các bản cập nhật gần đây, nhưng đáng lưu ý.
3. Độ trưởng thành hệ sinh thái. Hệ sinh thái quanh v3 rất lớn. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — mọi thứ đều được test với v3. Hỗ trợ v4 đang triển khai nhưng chưa phổ biến. Tôi phải submit PR cho một component library để sửa tương thích v4.
Bạn Có Nên Migrate?#
Đây là framework quyết định của tôi sau nhiều tuần sống với v4:
Migrate Ngay Nếu:#
- Bạn bắt đầu dự án mới (lựa chọn hiển nhiên — bắt đầu với v4)
- Dự án có ít custom plugin
- Bạn muốn lợi ích hiệu năng cho dự án lớn
- Bạn đã dùng pattern Tailwind hiện đại (slash opacity,
shrink-*, v.v.) - Bạn cần container queries và muốn không phải thêm plugin
Chờ Nếu:#
- Bạn phụ thuộc nặng vào plugin Tailwind bên thứ ba chưa hỗ trợ v4
- Bạn có cấu hình theme lập trình phức tạp
- Dự án ổn định và không phát triển tích cực (tại sao phải đụng?)
- Bạn đang giữa sprint tính năng (migrate giữa sprint, không phải trong sprint)
Không Migrate Nếu:#
- Bạn đang ở v2 hoặc cũ hơn (nâng cấp v3 trước, ổn định, rồi cân nhắc v4)
- Dự án kết thúc trong vài tháng tới (không đáng xáo trộn)
Ý Kiến Thật Của Tôi#
Cho dự án mới, v4 là lựa chọn hiển nhiên. Cấu hình CSS-first gọn hơn, engine nhanh hơn đáng kể, và tính năng mới (container queries, @starting-style, variant mới) thực sự hữu ích.
Cho dự án hiện có, tôi khuyến nghị cách tiếp cận từng bước:
- Bây giờ: Bắt đầu mọi dự án mới trên v4
- Sớm: Thí nghiệm bằng cách chuyển dự án nội bộ nhỏ sang v4
- Khi sẵn sàng: Migrate dự án production trong sprint yên tĩnh, với visual regression testing
Migration không đau đớn nếu bạn chuẩn bị. Codemod xử lý 80% công việc. 20% còn lại là thủ công nhưng đơn giản. Lên kế hoạch một ngày cho dự án vừa, hai đến ba ngày cho dự án lớn.
Tailwind v4 là thứ Tailwind lẽ ra đã nên là từ đầu. Cấu hình JavaScript luôn là nhượng bộ cho tooling thời đó. Cấu hình CSS-first, native cascade layers, engine Rust — đây không phải xu hướng, đây là framework bắt kịp nền tảng. Nền tảng web tốt hơn, và Tailwind v4 nghiêng vào nó thay vì chống lại nó.
Việc chuyển sang viết design token bằng CSS, kết hợp chúng với tính năng CSS, và để cascade của trình duyệt xử lý specificity — đó là hướng đi đúng. Mất bốn phiên bản lớn để đến đây, nhưng kết quả là phiên bản Tailwind mạch lạc nhất từ trước tới nay.
Bắt đầu dự án tiếp theo với nó. Bạn sẽ không muốn quay lại.