Tailwind CSS là gì: utility-first, cài v4 với Vite, Next.js và cách dùng không rối
Tailwind CSS là gì, vì sao nhiều team React bỏ BEM và CSS Modules để dùng utility class? Bài này hướng dẫn cài Tailwind v4.3 với Vite và Next.js, dùng @theme làm design token, dark mode, tái sử dụng style với cn() và khi nào không nên dùng.
Bạn nhận một project React đã chạy được hai năm. Mở thư mục styles ra thấy Button.module.css, button-new.css, .card__header--active, và một file overrides.css dài hơn nghìn dòng mà không ai dám xoá dòng nào, vì không biết class đó còn được dùng ở đâu. Muốn đổi padding của một cái nút, bạn phải nhảy qua ba file và vẫn lo sẽ làm vỡ trang khác.
Tailwind CSS sinh ra để giải đúng kiểu đau này. Lần đầu nhìn code Tailwind, nhiều người thấy "HTML bẩn quá". Sau vài tuần dùng thật trong một project React, phần lớn lại không muốn quay về. Bài này giải thích Tailwind CSS là gì, cách nó khác BEM và CSS Modules, cách cài bản v4 hiện tại cho Vite và Next.js, và những kỹ thuật giúp code Tailwind không biến thành một mớ class khó đọc.
Tailwind CSS là gì
Tailwind CSS là một CSS framework theo hướng utility-first: thay vì viết class theo ý nghĩa (.product-card) rồi định nghĩa CSS ở file khác, bạn ghép các class nhỏ, mỗi class làm đúng một việc, ngay trong markup.
<div class="mx-auto flex max-w-sm items-center gap-4 rounded-xl bg-white p-6 shadow-lg">
<img class="size-12 shrink-0" src="/logo.svg" alt="Logo" />
<div>
<p class="text-xl font-medium text-black">HoleTex</p>
<p class="text-slate-500">Bạn có tin nhắn mới!</p>
</div>
</div>p-6 là padding, rounded-xl là bo góc, gap-4 là khoảng cách giữa các phần tử con. Không có file CSS riêng cho component này. Khi build, Tailwind quét source code của bạn, tìm các class đã dùng và chỉ sinh ra CSS cho đúng những class đó.
Tại thời điểm viết (tháng 10/2026), bản mới nhất là Tailwind CSS v4.3, phát hành tháng 5/2026. Tháng 9/2026, Tailwind Labs thông báo gia nhập Shopify; theo bài công bố, Tailwind CSS vẫn giữ giấy phép MIT và tiếp tục được team cũ phát triển.
Utility-first khác BEM và CSS Modules ở đâu
Ba cách tiếp cận này giải cùng một bài toán: làm sao để CSS không giẫm chân nhau khi project lớn lên.
| BEM | CSS Modules | Tailwind (utility-first) | |
|---|---|---|---|
| Tránh xung đột tên | Quy ước đặt tên thủ công (block__element--modifier) | Build tool tự hash tên class | Không cần đặt tên, class là thuộc tính |
| Style nằm ở đâu | File CSS riêng | File .module.css cạnh component | Ngay trong JSX/HTML |
| Xoá component | Phải nhớ xoá CSS tương ứng | Xoá cả file module | Xoá markup là xong |
| Thống nhất design | Phụ thuộc kỷ luật team | Phụ thuộc kỷ luật team | Bị giới hạn trong design token (spacing, màu...) |
| Kích thước CSS | Tăng theo số component | Tăng theo số component | Tăng theo số class khác nhau đã dùng |
| Điểm yếu | Tên dài, dễ lệch quy ước | Vẫn phải đặt tên, nhảy file | Markup dài, cần làm quen |
Điểm khác cốt lõi: với BEM hay CSS Modules, mỗi component mới thường kéo theo CSS mới. Với Tailwind, component mới chủ yếu tái dùng các class đã có, nên CSS output tăng chậm hơn nhiều so với số component. Và vì style nằm ngay trong markup, bạn đọc một component là biết nó trông thế nào, không phải mở thêm file.
Cái giá phải trả là markup dài hơn, và bạn phải học bộ từ vựng class. Phần sau của bài nói cách xử lý cả hai.
Cài Tailwind CSS v4 với Vite
Tailwind v4 không còn bắt buộc file tailwind.config.js như v3: cấu hình giờ nằm ngay trong CSS (CSS-first config). Với project Vite (React, Vue, Svelte...), docs chính thức dùng plugin riêng:
npm create vite@latest my-app
cd my-app
npm install tailwindcss @tailwindcss/viteThêm plugin vào vite.config.ts (project React thì giữ plugin react() có sẵn):
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})Trong file CSS chính (ví dụ src/index.css), xoá nội dung cũ và thay bằng một dòng:
@import "tailwindcss";Chạy npm run dev, thử <h1 className="text-3xl font-bold underline">Hello</h1> là thấy kết quả. Không còn ba dòng @tailwind base; @tailwind components; @tailwind utilities; như v3, cũng không cần khai báo content để chỉ đường cho Tailwind: v4 tự phát hiện source file, bỏ qua những gì trong .gitignore, node_modules, file binary, file CSS và lock file của package manager.
Cài Tailwind CSS v4 với Next.js
Nếu bạn mới làm quen Next.js, đọc trước bài Next.js là gì. Nếu bạn tạo project mới bằng npx create-next-app@latest và chọn bộ cài đặt mặc định, Tailwind v4 đã được cấu hình sẵn: có postcss.config.mjs và dòng @import "tailwindcss"; trong app/globals.css. Các bước dưới đây dành cho project Next.js cũ chưa có Tailwind. Với Next.js (docs hiện ở bản 16.4), Tailwind chạy qua PostCSS:
npm install -D tailwindcss @tailwindcss/postcssTạo postcss.config.mjs ở thư mục gốc:
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}Trong app/globals.css:
@import "tailwindcss";Và import file đó một lần ở root layout:
// app/layout.tsx
import './globals.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="vi">
<body className="bg-white text-slate-900">{children}</body>
</html>
)
}Docs của Next.js khuyên dùng Tailwind cho phần lớn nhu cầu styling, và chỉ dùng CSS Modules khi utility class không đủ.
Lưu ý trước khi nâng cấp: v4 yêu cầu trình duyệt hiện đại (Chrome 111, Safari 16.4, Firefox 128 trở lên) vì dùng các tính năng CSS mới như color-mix(). Nếu sản phẩm phải chạy trên trình duyệt cũ hơn, hãy ở lại Tailwind v3. Project v3 muốn lên v4 thì chạy npx @tailwindcss/upgrade (cần Node.js 20 trở lên), rồi review diff thật kỹ: một số class đã đổi tên, ví dụ shadow-sm thành shadow-xs, rounded thành rounded-sm, outline-none thành outline-hidden, ring giờ chỉ dày 1px (muốn giữ 3px như v3 thì dùng ring-3), và màu viền mặc định giờ là currentColor thay vì gray-200.
Responsive, dark mode và state: tất cả là variant
Mọi utility trong Tailwind đều có thể gắn điều kiện bằng variant, viết dạng variant:class.
Responsive theo kiểu mobile-first
Class không có tiền tố áp dụng cho mọi màn hình. Tiền tố sm, md, lg, xl, 2xl nghĩa là "từ breakpoint này trở lên" (mặc định lần lượt 40rem, 48rem, 64rem, 80rem, 96rem).
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{courses.map((c) => (
<CourseCard key={c.id} course={c} />
))}
</div>Một cột trên điện thoại, hai cột từ tablet, bốn cột trên desktop. Lỗi hay gặp nhất của người mới: viết sm:text-center với ý "căn giữa trên mobile". Sai, vì sm: nghĩa là từ 40rem trở lên. Muốn style cho mobile thì viết class không tiền tố, rồi ghi đè ở breakpoint lớn hơn.
Hover, focus, group và peer
<a href="/blog" className="group rounded-lg p-4 hover:bg-slate-900">
<h3 className="font-semibold text-slate-900 group-hover:text-white">Blog</h3>
<p className="text-slate-500 group-hover:text-slate-300">Bài mới mỗi tuần</p>
</a>
<div>
<input type="email" className="peer rounded border px-3 py-2" />
<p className="invisible text-sm text-red-600 peer-invalid:visible">
Email không hợp lệ
</p>
</div>group cho phép con đổi style khi cha được hover. peer cho phép một phần tử đổi style theo trạng thái của phần tử anh em đứng trước nó, ở đây là hiện thông báo lỗi khi input không hợp lệ, không cần một dòng JavaScript nào. Các variant ghép được với nhau: dark:md:hover:bg-slate-800. Ngoài ra còn aria-* và data-*, rất tiện khi style component headless: data-[state=open]:rotate-180.
Dark mode
Mặc định, dark: đi theo cài đặt hệ điều hành (prefers-color-scheme). Nếu cần nút bật tắt thủ công, khai báo lại variant dark trong CSS:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));Từ đó, thêm class dark vào thẻ <html> là các class dark:bg-slate-950 có hiệu lực. Muốn dùng data attribute (<html data-theme="dark">) thì khai báo: @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
Design token với @theme
Đây là thay đổi đáng giá nhất của v4. Design token (màu thương hiệu, font, breakpoint) được khai báo bằng CSS variable trong khối @theme, và Tailwind tự sinh utility tương ứng:
@import "tailwindcss";
@theme {
--color-brand-500: oklch(0.58 0.22 25);
--color-brand-600: oklch(0.5 0.21 25);
--font-display: "Space Grotesk", sans-serif;
--breakpoint-3xl: 120rem;
}Khai báo xong là có ngay bg-brand-500, text-brand-600, border-brand-500, font-display và variant 3xl:. Không cần sửa file JavaScript nào.
Các biến này cũng được xuất ra thành CSS variable thật trên :root, nên bạn dùng được trong CSS thường (color: var(--color-brand-500)) hay inline style. Lưu ý: mặc định Tailwind chỉ xuất những biến thực sự được dùng. Nếu cần đọc token từ JavaScript (getComputedStyle, chart library), khai báo @theme static { ... } để Tailwind luôn sinh đủ biến. Nhờ vậy design token là một nguồn duy nhất cho cả Tailwind lẫn phần còn lại của app.
Muốn bỏ hẳn bảng màu mặc định để team chỉ dùng được màu của design system:
@theme {
--color-*: initial;
--color-white: #fff;
--color-ink: #0f1524;
--color-brand: #d01325;
}Phân biệt nhanh: biến trong @theme sinh ra utility class; biến khai báo trong :root chỉ là CSS variable thường. Biến nào không cần thành class thì để ở :root. Và @theme phải nằm ở cấp cao nhất, không lồng trong selector hay media query.
Khi cần tạo utility riêng mà vẫn dùng được với mọi variant, dùng @utility:
@utility content-auto {
content-visibility: auto;
}Giờ content-auto, lg:content-auto đều chạy.
Tái sử dụng style: component trước, @apply sau
Câu hỏi đầu tiên ai dùng Tailwind cũng gặp: "Mười cái nút giống nhau, chẳng lẽ copy chuỗi class mười lần?". Docs Tailwind đưa ra thứ tự ưu tiên khá rõ:
- Vòng lặp: nếu các phần tử render từ một mảng, chuỗi class chỉ viết một lần.
- Multi-cursor: trùng lặp trong cùng một file, sửa một lúc bằng editor là đủ.
- Component: dùng lại ở nhiều file thì tách thành component React. Đây là cách chính.
- CSS tuỳ biến: chỉ khi tách component là quá nặng, ví dụ style cho HTML sinh từ thư viện bên thứ ba.
Với React, component là đơn vị tái sử dụng tự nhiên. Pattern phổ biến (shadcn/ui cũng dùng) là kết hợp clsx để ghép class có điều kiện và tailwind-merge để xử lý class xung đột:
npm install clsx tailwind-merge// lib/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}(shadcn/ui tạo sẵn hàm này trong lib/utils.ts, nếu đã cài shadcn thì dùng luôn.)
Vì sao cần twMerge? Nếu Button có sẵn px-4 và chỗ dùng truyền thêm px-6, chuỗi class chứa cả hai, và class nào thắng phụ thuộc vào thứ tự trong file CSS sinh ra chứ không phải thứ tự bạn viết. twMerge loại class bị ghi đè, giữ lại class sau cùng. Bản tailwind-merge hiện tại hỗ trợ Tailwind v4.0 đến v4.3; project còn ở v3 thì dùng tailwind-merge 2.6.0.
Một Button có variant hoàn chỉnh:
// components/button.tsx
import { cn } from '@/lib/cn'
const variants = {
primary: 'bg-brand-500 text-white hover:bg-brand-600',
outline: 'border border-slate-300 text-slate-900 hover:bg-slate-100',
ghost: 'text-slate-700 hover:bg-slate-100',
} as const
const sizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
} as const
type ButtonProps = React.ComponentProps<'button'> & {
variant?: keyof typeof variants
size?: keyof typeof sizes
}
export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-colors',
'focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50',
variants[variant],
sizes[size],
className,
)}
{...props}
/>
)
}<Button>Đăng ký</Button>
<Button variant="outline" size="sm" className="px-6">Xem thêm</Button>Ở dòng thứ hai, px-6 truyền từ ngoài sẽ thắng px-3 của size sm nhờ twMerge.
Để ý cách viết map variants: mỗi giá trị là tên class đầy đủ. Đừng bao giờ viết ` `bg-${color}-500 `. Tailwind quét source như văn bản thuần, nó không chạy code của bạn, nên chuỗi `bg-red-500 không xuất hiện ở đâu thì CSS cho nó cũng không được sinh ra. Đây là bug "class đúng mà không ăn" phổ biến nhất.
Còn @apply thì sao?
@apply vẫn tồn tại và hữu ích khi bạn phải style HTML không kiểm soát được, ví dụ widget của thư viện bên thứ ba:
.select2-dropdown {
@apply rounded-b-lg shadow-md;
}Nhưng dùng @apply để tạo .btn, .card cho mọi thứ thì bạn đang quay lại viết BEM với cú pháp khác, và mất lợi ích "đọc markup là biết style". Trong React, ưu tiên component. Nếu buộc phải dùng @apply trong CSS Modules hoặc <style> của Vue/Svelte, phải thêm @reference "../app.css"; ở đầu file để file đó thấy được theme. Docs Tailwind thậm chí gợi ý cách gọn hơn là dùng thẳng CSS variable: background: var(--color-blue-500);.
Những lời chê phổ biến và cách team xử lý
"Class dài, khó đọc." Đúng, nhất là với component có nhiều state. Cách xử lý thực tế: tách component nhỏ; chia chuỗi class theo nhóm ý nghĩa (layout, màu, state) thành nhiều đối số của cn() như ví dụ Button; cài extension Tailwind CSS IntelliSense để có autocomplete và hover xem CSS thật.
"Mỗi người sắp xếp class một kiểu, review diff rất mệt." Dùng prettier-plugin-tailwindcss để tự sắp xếp theo thứ tự thống nhất. Với v4 phải chỉ đường tới file CSS chính, và có thể bật sắp xếp bên trong cn():
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/index.css",
"tailwindFunctions": ["cn", "clsx"]
}"Đây chẳng khác gì inline style." Khác: utility bị giới hạn trong design token, và có variant cho responsive, hover, dark mode mà inline style không làm được.
"Lạm dụng giá trị arbitrary." w-[317px], text-[#3b3b3b] rải khắp nơi là dấu hiệu design system đang bị phá. Quy ước nên có trong team: giá trị lặp lại hai lần trở lên thì đưa vào @theme.
"Dùng chung với Sass được không?" Docs Tailwind v4 khuyên không. Tailwind v4 đã tự xử lý import, nesting và biến, nên Sass chủ yếu thêm một bước build. Với CSS Modules, docs Tailwind cũng không khuyến khích dùng chung: Tailwind phải chạy riêng cho từng file module (build chậm hơn), và module không thấy theme nếu thiếu @reference. Nếu vẫn cần CSS Modules cho vài trường hợp đặc biệt, hãy dùng CSS variable (var(--color-brand-500)) thay vì @apply.
Về performance: CSS của Tailwind thường không phải nút thắt. App React chậm thường do render và bundle JavaScript; xem bài Tối ưu performance React.
Khi nào không nên dùng Tailwind
- Trang nội dung dài từ CMS hoặc Markdown: bạn không gắn class vào từng thẻ
<p>được. Dùng plugin typography hoặc viết CSS cho vùng nội dung đó. - Team đã có design system ổn định bằng CSS Modules hoặc CSS-in-JS: chuyển đổi tốn công mà lợi ích không rõ. Đừng rewrite chỉ vì trend.
- Phải hỗ trợ trình duyệt cũ hơn Safari 16.4 hoặc Chrome 111: v4 không chạy đúng. Dùng Tailwind v3 hoặc giải pháp khác.
- Trang tĩnh nhỏ một vài file HTML: CSS thuần có thể đơn giản hơn so với thêm một bước build.
- Team chưa vững CSS nền tảng: Tailwind không thay thế việc hiểu flexbox, grid, box model.
flex items-centerchỉ dễ khi bạn biếtalign-itemslàm gì.
Ngược lại, nếu bạn làm app React hoặc Next.js mới, có nhiều component, nhiều người cùng code, Tailwind thường là lựa chọn ít rủi ro: nhiều bộ UI phổ biến như shadcn/ui hay Tailwind Plus được xây trên Tailwind, và tài liệu, ví dụ rất nhiều.
Checklist bắt đầu Tailwind cho project React
Nếu bạn đang học React và muốn đi từ component cơ bản tới dự án thật có design system, routing, state và deploy, khoá React PRO của HoleTex đi theo đúng lộ trình đó, với Tailwind là phần styling xuyên suốt.
Bài liên quan
- React là gì: hiểu đúng trước khi học
- Next.js là gì
- Tối ưu performance React
- React hay Next.js: chọn cái nào
- Lộ trình học ReactJS 2026
Nguồn tham khảo: Installing with Vite (tailwindcss.com), Install with Next.js (tailwindcss.com), CSS trong Next.js (nextjs.org), Theme variables (tailwindcss.com), Hover, focus and other states (tailwindcss.com), Dark mode (tailwindcss.com), Reusing styles (tailwindcss.com), Functions and directives (tailwindcss.com), Detecting classes in source files (tailwindcss.com), Compatibility (tailwindcss.com), Upgrade guide (tailwindcss.com), create-next-app (nextjs.org), Tailwind CSS v4.3 (tailwindcss.com), Tailwind Labs is joining Shopify (tailwindcss.com), tailwind-merge (github.com), prettier-plugin-tailwindcss (github.com). Cập nhật 2026-10-08.