Tailwind CSS — "Lên Đồ" Cho Giao Diện Siêu Tốc 🎨¶
Phong Cách "Utility-first" Thực Chất Là Gì?¶
Cách làm ngày xưa (CSS truyền thống): Bạn phải vặn óc nghĩ ra một cái tên (class) cho thẻ HTML, rồi lật sang một file CSS riêng biệt để viết các quy tắc làm đẹp cho nó.
/* Viết class với tên có nghĩa, style riêng biệt nằm một nơi */
.card {
display: flex;
flex-direction: column;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #111827;
}
Cách mới với Tailwind (Utility-first): Bạn không cần tạo file CSS riêng nữa! Bạn sẽ ném trực tiếp các class "miếng ghép nhỏ" (mỗi class làm đúng một việc) thẳng vào HTML/JSX.
<div class="flex flex-col p-4 bg-white rounded-lg shadow">
<h3 class="text-lg font-semibold text-gray-900">Tiêu đề</h3>
</div>
Tại Sao Lại Chọn Cách Làm Này Nhỉ? 🤔¶
Nỗi Khổ Với Cách Truyền Thống¶
- Vắt óc nghĩ tên: Bạn sẽ hay bị bí từ khi phải đặt tên
.card,.card-wrapper,.card-inner,.card-content... rất nhức đầu. - File CSS phình to như quả bóng: Thêm tính năng là thêm CSS. Nhưng lúc xóa tính năng lại không dám xóa CSS vì sợ... lỡ nó đang được dùng ở chỗ khác thì toang.
- Đụng độ toàn cầu (Global scope): Class
.titlelỡ viết ở trang A có thể vô tình làm hỏng giao diện ở trang B. - Phân tâm: Đang gõ HTML lại phải nhảy sang file CSS, mất tập trung.
Điểm Sung Sướng Của Tailwind¶
- Khỏi nghĩ tên: Tên class chính là công dụng của nó luôn.
- Kích thước siêu nhẹ: CSS không bao giờ phình to bừa bãi vì nó chỉ gom đúng những class bạn thực sự dùng (nhờ tính năng PurgeCSS).
- An toàn tuyệt đối: Viết ngay trong thẻ nào thì chỉ tác dụng lên thẻ đó, không sợ lây lan (conflict).
- Thiết kế cực chuẩn: Tailwind ép bạn dùng một bộ kích thước cố định (4, 8, 12, 16px...) nên nhìn trang web sẽ rất đồng bộ và chuyên nghiệp.
Đương Nhiên, Nó Cũng Có Khuyết Điểm¶
- Nhìn hơi ngợp: Chuỗi class đôi khi dài dằng dặc kiểu
className="flex items-center justify-between p-4 bg-white rounded-lg...". - Cần thời gian làm quen: Bạn phải chịu khó học thuộc (hoặc dùng tool gợi ý) tên class của nó.
- Làm file HTML/JSX nhìn rối hơn: Do chứa quá nhiều class.
Những Khái Niệm Cốt Lõi Bỏ Túi 🎒¶
1. Hệ Thống Thước Đo (Spacing Scale)¶
Tailwind rất thích số 4. Các class đệm (padding - p) hay cách lề (margin - m) đều lấy 4px làm gốc:
2. Giao Diện Co Giãn Đa Màn Hình (Responsive)¶
Tailwind thiết kế theo chuẩn "Ưu tiên điện thoại" (Mobile first). Class viết không có tiền tố sẽ áp dụng cho điện thoại. Muốn đổi ở màn to hơn thì thêm tiền tố vào trước:
<div className="
grid grid-cols-1 // Trên điện thoại: Hiển thị 1 cột
md:grid-cols-2 // Màn hình tablet (>= 768px): Chia làm 2 cột
lg:grid-cols-3 // Màn hình laptop (>= 1024px): Chia làm 3 cột
gap-4
">
| Tiền tố (Prefix) | Chiều rộng tối thiểu | Giải thích |
|---|---|---|
| (Không có) | 0px | Màn hình điện thoại |
sm: |
640px | Điện thoại to / Tablet dọc |
md: |
768px | Tablet ngang |
lg: |
1024px | Laptop nhỏ |
xl: |
1280px | Màn hình máy bàn |
2xl: |
1536px | Màn hình siêu bự |
3. Trạng Thái Của Nút Bấm (State Variants)¶
Muốn đổi màu khi di chuột qua? Hay làm mờ khi bị khóa? Chỉ cần dùng từ khóa:
<button className="
bg-indigo-600
hover:bg-indigo-700 // Đổi màu khi đưa chuột vào (hover)
focus:outline-none // Bỏ viền đen xấu xí khi click
focus:ring-2 // Thêm viền phát sáng tự chế
focus:ring-indigo-500
disabled:opacity-50 // Làm mờ 50% khi bị khóa (disabled)
disabled:cursor-not-allowed // Đổi con trỏ chuột thành dấu cấm
transition-colors // Làm hiệu ứng chuyển màu mượt mà
">
4. Chế Độ Giao Diện Tối (Dark Mode)¶
Các "Mảnh Ghép Lego" Hay Dùng Trong Dự Án (Patterns) 🧩¶
Vòng xoay tải trang (Loading Spinner)¶
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600" />
</div>
Các Kiểu Nút Bấm (Button)¶
// Nút chính (Nổi bật)
<button className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors font-medium">
Thêm giao dịch
</button>
// Nút phụ (Nhẹ nhàng)
<button className="border border-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors">
Hủy
</button>
// Nút cảnh báo (Xóa/Báo lỗi)
<button className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">
Xóa
</button>
// Nút chỉ có Icon (Nút tròn nhỏ)
<button className="p-2 rounded-full hover:bg-gray-100 text-gray-500 hover:text-gray-700">
<XMarkIcon className="h-5 w-5" />
</button>
Khung Chứa Dữ Liệu (Card)¶
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Tổng quan</h2>
{/* Nội dung nhét vào đây */}
</div>
Ô Nhập Liệu (Input)¶
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Số tiền
</label>
<input
type="number"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent
placeholder:text-gray-400"
placeholder="0"
/>
</div>
</div>
Menu Chuyển Tab (Tab Navigation)¶
const tabs = ['Giao dịch', 'Quỹ', 'Tổng kết', 'Thành viên'];
<div className="flex border-b border-gray-200 overflow-x-auto">
{tabs.map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`
px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors
${activeTab === tab
? 'border-indigo-600 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}
`}
>
{tab}
</button>
))}
</div>
Khung Tràn Màn Hình (Modal / Overlay)¶
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
{/* Nền đen mờ phía sau (Backdrop) */}
<div
className="absolute inset-0 bg-black/50"
onClick={onClose}
/>
{/* Khung nội dung chính nổi lên trên */}
<div className="relative bg-white w-full sm:max-w-md rounded-t-2xl sm:rounded-2xl p-6 shadow-xl">
{/* ... */}
</div>
</div>
Tô Màu Số Tiền (Đỏ = Chi, Xanh = Thu)¶
function AmountBadge({ type, amount, currency }) {
const isExpense = type === 'expense';
return (
<span className={`font-semibold ${isExpense ? 'text-red-600' : 'text-green-600'}`}>
{isExpense ? '-' : '+'}{formatCurrency(amount, currency)}
</span>
);
}
Cài Đặt Tailwind Nhanh ⚙️¶
// File: web/tailwind.config.js
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}', // Bảo Tailwind đi quét các file này để nhặt class
],
theme: {
extend: {
// Bạn có thể chế thêm màu riêng cho dự án ở đây
colors: {
brand: '#6366f1', // Tạo màu brand (Tương đương mã màu indigo-500)
},
},
},
plugins: [],
}
/* File: web/src/index.css */
@tailwind base; /* Nơi làm sạch các định dạng HTML cũ */
@tailwind components; /* Nơi chứa các class to do bạn tự gom */
@tailwind utilities; /* Nơi chứa 1001 class nhỏ gọn của Tailwind */
Thay Đổi Class Linh Hoạt (Conditional Classes) 🔀¶
Khi bạn muốn một cái nút đổi màu tùy thuộc vào việc nó đang bật hay tắt, hãy xài dấu hoặc thư việnclsx`:
// Cách 1: Dùng chuỗi kẹp biến (Dự án mình xài cách này)
<div className={`
px-4 py-2 rounded-lg font-medium
${isActive ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:bg-gray-100'}
`}>
// Cách 2: Dùng thư viện clsx (Nhìn chuyên nghiệp hơn, tùy chọn)
import clsx from 'clsx';
<div className={clsx(
'px-4 py-2 rounded-lg font-medium',
isActive && 'bg-indigo-600 text-white',
!isActive && 'text-gray-600 hover:bg-gray-100'
)}>
Phép Thuật Giảm Cân (Tree-shaking) — Sao File CSS Nó Nhỏ Xíu Vậy? 🍃¶
Ban đầu, Tailwind ôm trong mình hàng ngàn class đồ sộ:
Nhưng khi bạn "đóng gói" (build) dự án, Vite và Tailwind sẽ lướt qua code của bạn. Nó thấy bạn chỉ xài tầm 20-30 class, nó sẽ thẳng tay vứt hết hàng ngàn class không dùng kia đi.
Đó là lý do website chạy bằng Tailwind load cực kỳ nhanh.