Mục lục

Deep Dive: Design Tokens Strategy

Tại sao không nên dùng hex code trực tiếp? Chiến lược đặt tên Semantic Token và cách map chúng vào Tailwind CSS để hỗ trợ Dark Mode hoàn hảo.

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:

css:
/* Bad */
.button {
  background-color: #3b82f6; /* Blue 500 */
  color: white;
  padding: 16px;
}

Hậu quả:

  1. 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.
  2. Không thể hỗ trợ Dark Mode: Giá trị màu cố định không tự đổi theo theme hệ thống.
  3. 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: #3b82f6
  • slate-900: #0f172a
  • space-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:

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.

javascript:
// 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.

tsx:
// 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.

Quảng cáo
mdhorizontal