Trước khi viết dòng code UI nào, chúng ta cần thống nhất ngôn ngữ thiết kế. Design Tokens chính là cầu nối giữa Designer (Figma) và Developer (Mã nguồn).
1. Vấn đề của Hardcoded Values
Hãy tưởng tượng bạn code như thế này:
/* Bad */
.button {
background-color: #3b82f6; /* Blue 500 */
color: white;
padding: 16px;
}Hậu quả:
- Không thể đổi Brand: Sếp muốn đổi màu chủ đạo từ Xanh sang Cam? Bạn phải đi Tìm & Thay thế 500 file.
- Không thể hỗ trợ Dark Mode: Giá trị màu cố định không tự đổi theo theme hệ thống.
- Inconsistent: Dev A dùng
#3b82f6, Dev B dùng màu gần giống#3b82f5.
2. Token Architecture (3 Layers)
Chúng ta áp dụng mô hình 3 tầng tiêu chuẩn công nghiệp (như Adobe Spectrum, Salesforce Lightning).
Layer 1: Primitive Tokens (Global)
Đây là bảng màu gốc (Palette). Tên gọi mô tả màu sắc thực tế.
blue-500:#3b82f6slate-900:#0f172aspace-4:1rem(16px)
Layer 2: Semantic Tokens (Alias)
Đây là tầng quan trọng nhất. Tên gọi mô tả mục đích sử dụng.
bg-primary:blue-500(Nền chính)text-body:slate-900(Chữ đoạn văn)bg-destructive:red-600(Nền nút xóa)
Layer 3: Component Tokens (Optional)
Token dành riêng cho một component cụ thể (thường ít dùng hơn để tránh quá tải).
btn-primary-bg:bg-primary
3. Implementation với Tailwind & CSS Vars
Chúng ta sẽ implement tầng Semantic bằng CSS Variables để hỗ trợ đổi theme tại runtime.
Bước 1: Định nghĩa trong globals.css
Trong file packages/ui/src/globals.css:
@layer base {
:root {
/* ----- Primitives (Ẩn danh, không map trực tiếp vào Tailwind) ----- */
--color-blue-500: 217 91% 60%; /* Dùng hệ HSL để dễ chỉnh độ sáng */
--color-slate-900: 222 47% 11%;
--color-slate-50: 210 40% 98%;
--color-red-600: 0 72% 51%;
/* ----- Semantics (Light Mode is Default) ----- */
/* Backgrounds */
--background: 0 0% 100%; /* Trắng */
--foreground: var(--color-slate-900); /* Đen nhạt */
/* Brands */
--primary: var(--color-blue-500);
--primary-foreground: var(--color-slate-50); /* Chữ trắng trên nền xanh */
/* Errors */
--destructive: var(--color-red-600);
--destructive-foreground: var(--color-slate-50);
/* Borders */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
/* Secondary */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Popover */
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
/* Card */
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--radius: 0.5rem;
}
.dark {
/* ----- Semantics (Dark Mode overrides) ----- */
--background: var(--color-slate-900); /* Nền đen */
--foreground: var(--color-slate-50); /* Chữ trắng */
/* Primary giữ nguyên hoặc chỉnh sáng hơn chút */
--primary: 217 91% 65%;
--primary-foreground: var(--color-slate-900); /* Chữ đen trên nền xanh sáng */
/* Secondary Dark */
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
/* Muted Dark */
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
/* Borders Dark */
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
/* Accent Dark */
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
/* Popover Dark */
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
/* Card Dark */
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
}
}Tại sao dùng HSL?
Tailwind cần HSL không có hsl(...) bao quanh để có thể dùng modifier opacity (ví dụ: bg-primary/50). Code tailwind sẽ tự wrap lại.
Bước 2: Config Tailwind (tailwind-preset.js)
Map các biến Semantic vào config của Tailwind.
// packages/config/tailwind-preset.js
module.exports = {
theme: {
extend: {
colors: {
// Map Semantic Names -> CSS Variables
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
}
}
}Bước 3: Sử dụng
Bây giờ dev không bao giờ phải nhớ mã màu Hex nữa.
// Luôn đúng dù ở Light hay Dark mode
<div className="bg-background text-foreground border border-border">
<button className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md">
Lưu thay đổi
</button>
<p className="text-muted-foreground text-sm">
Dữ liệu sẽ được lưu vào database.
</p>
</div>Tổng kết
Tư duy "Tokens First" giúp hệ thống bền vững. Khi Re-brand diễn ra 2 năm sau, bạn chỉ cần sửa 1 file globals.css thay vì refactor 10.000 dòng code.