Tailwind CSS

Customization, Theme & Configuration

Tailwind CSS v4 CSS-first config — @theme, @utility, @plugin, @custom-variant, custom colors, fonts, breakpoints. Bài tập custom design system.

1. CSS-first Configuration (v4.0 paradigm shift)

Tailwind CSS v4 đánh dấu sự thay đổi triệt để trong cách cấu hình: bỏ file JavaScript tailwind.config.js, chuyển mọi thứ vào CSS. Đây là paradigm shift quan trọng nhất của v4.

Câu PV:"Tại sao Tailwind chuyển từ JS config sang CSS-first config?"

"Ba lý do chính: (1) Native CSS: Token được định nghĩa trong CSS — nơi tự nhiên nhất cho design tokens. (2) Performance: Rust engine parse CSS nhanh hơn parse JS config nhiều lần. (3) Interop: CSS custom properties có thể dùng ở bất kỳ đâu — JS framework nào cũng đọc được, không lock-in vào Tailwind."

1.1. So sánh Old Way vs New Way

// === OLD WAY (Tailwind v3) ===
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          light: '#60a5fa',
          DEFAULT: '#3b82f6',
          dark: '#1d4ed8',
        }
      },
      fontFamily: {
        display: ['Inter', 'sans-serif'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      }
    }
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
  ]
}
/* === NEW WAY (Tailwind v4) === */
/* main.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

@theme {
  --color-brand-light: #60a5fa;
  --color-brand: #3b82f6;
  --color-brand-dark: #1d4ed8;

  --color-brand-50: oklch(0.97 0.01 250);
  --color-brand-100: oklch(0.93 0.03 250);
  --color-brand-200: oklch(0.85 0.06 250);
  --color-brand-300: oklch(0.75 0.10 250);
  --color-brand-400: oklch(0.65 0.15 250);
  --color-brand-500: oklch(0.55 0.20 250);
  --color-brand-600: oklch(0.48 0.18 250);
  --color-brand-700: oklch(0.40 0.15 250);
  --color-brand-800: oklch(0.30 0.12 250);
  --color-brand-900: oklch(0.22 0.08 250);
  --color-brand-950: oklch(0.15 0.05 250);

  --font-display: "Inter", sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  --spacing-18: 4.5rem;
  --spacing-88: 22rem;
}

1.2. Cơ chế Auto-Expose CSS Variables

Mọi token trong @theme đều tự động được expose thành CSS custom properties:

@theme {
  --color-primary: oklch(0.55 0.2 250);
  --font-display: "Inter", sans-serif;
}

/* Tự động generate: */
/* --color-primary: oklch(0.55 0.2 250); */
/* --font-display: "Inter", sans-serif; */

/* Bạn có thể dùng ở bất kỳ đâu: */
.my-custom-class {
  background-color: var(--color-primary);
  font-family: var(--font-display);
}
Điều này có ý nghĩa gì? Bạn có thể dùng design tokens của Tailwind trong bất kỳ ngữ cảnh nào — inline style, CSS module, styled-components, thậm chí trong JavaScript qua getComputedStyle(). Không lock-in vào Tailwind class.

1.3. Namespace Trong @theme

Tailwind v4 phân biệt các loại token qua prefix:

PrefixLoại tokenVí dụ
--color-*Màu sắc--color-primary: blue
--font-*Font family--font-display: "Inter"
--text-*Font size + line height--text-lg: 1.125rem
--font-weight-*Font weight--font-weight-medium: 500
--tracking-*Letter spacing--tracking-wide: 0.025em
--leading-*Line height--leading-relaxed: 1.625
--spacing-*Spacing scale--spacing-18: 4.5rem
--radius-*Border radius--radius-xl: 0.75rem
--shadow-*Box shadow--shadow-card: 0 2px 8px rgba(0,0,0,0.1)
--breakpoint-*Breakpoints--breakpoint-3xl: 120rem
--animate-*Animations--animate-slide-in: slideIn 0.3s ease
--container-*Container query breakpoints--container-md: 28rem

1.4. OKLCH Colors — Cách Viết Màu Trong v4

OKLCH là color space mặc định của Tailwind v4. Cú pháp:

oklch(L C H)
L = Lightness    (0-1 hoặc 0%-100%)
C = Chroma       (0-0.4, màu càng rực khi C càng cao)
H = Hue          (0-360, góc trên bánh xe màu)
@theme {
  /* OKLCH — cách viết recommended trong v4 */
  --color-blue-500: oklch(0.62 0.19 255);
  --color-red-500: oklch(0.59 0.21 25);
  --color-green-500: oklch(0.63 0.19 145);

  /* Vẫn hỗ trợ HEX, RGB, HSL */
  --color-legacy: #3b82f6;
  --color-rgb: rgb(59 130 246);
  --color-hsl: hsl(217 91% 60%);
}
Tại sao OKLCH? OKLCH có perceptual uniformity — hai màu có cùng Chroma/Lightness sẽ trông "rực rỡ như nhau" với mắt người, điều mà HSL/sRGB làm không tốt. Điều này giúp tạo color palette nhất quán hơn. Cộng thêm P3 wide gamut — màu rực hơn trên màn hình hiện đại.
/* Ví dụ: tạo color scale với OKLCH */
@theme {
  /* Brand color scale — chỉ thay đổi Lightness */
  --color-brand-50:  oklch(0.97 0.02 260);
  --color-brand-100: oklch(0.92 0.04 260);
  --color-brand-200: oklch(0.85 0.07 260);
  --color-brand-300: oklch(0.75 0.11 260);
  --color-brand-400: oklch(0.66 0.16 260);
  --color-brand-500: oklch(0.55 0.20 260); /* Base */
  --color-brand-600: oklch(0.46 0.18 260);
  --color-brand-700: oklch(0.38 0.15 260);
  --color-brand-800: oklch(0.28 0.11 260);
  --color-brand-900: oklch(0.20 0.07 260);
  --color-brand-950: oklch(0.14 0.04 260);
}

1.5. BÀI TẬP 1 — Tạo Complete Brand Theme

Yêu cầu: Tạo một brand theme hoàn chỉnh cho một startup fintech với:

  • Màu chính (primary): xanh dương đậm
  • Màu phụ (secondary): xanh lá
  • Accent: cam
  • Font: Inter (headings) + system font (body)
  • Spacing custom

Code đầy đủ:

/* fintech-theme.css */
@import "tailwindcss";

@theme {
  /* === COLORS === */
  
  /* Primary — Deep Blue */
  --color-primary-50:  oklch(0.97 0.02 255);
  --color-primary-100: oklch(0.92 0.04 255);
  --color-primary-200: oklch(0.84 0.07 255);
  --color-primary-300: oklch(0.74 0.12 255);
  --color-primary-400: oklch(0.64 0.16 255);
  --color-primary-500: oklch(0.54 0.20 255);
  --color-primary-600: oklch(0.46 0.18 255);
  --color-primary-700: oklch(0.37 0.15 255);
  --color-primary-800: oklch(0.28 0.11 255);
  --color-primary-900: oklch(0.20 0.07 255);
  --color-primary-950: oklch(0.14 0.04 255);

  /* Secondary — Emerald Green */
  --color-secondary-50:  oklch(0.97 0.02 155);
  --color-secondary-100: oklch(0.92 0.04 155);
  --color-secondary-200: oklch(0.85 0.07 155);
  --color-secondary-300: oklch(0.75 0.12 155);
  --color-secondary-400: oklch(0.65 0.17 155);
  --color-secondary-500: oklch(0.56 0.20 155);
  --color-secondary-600: oklch(0.47 0.18 155);
  --color-secondary-700: oklch(0.38 0.15 155);
  --color-secondary-800: oklch(0.28 0.11 155);
  --color-secondary-900: oklch(0.20 0.07 155);

  /* Accent — Warm Orange */
  --color-accent-50:  oklch(0.97 0.02 60);
  --color-accent-100: oklch(0.93 0.04 60);
  --color-accent-200: oklch(0.87 0.08 60);
  --color-accent-300: oklch(0.78 0.13 60);
  --color-accent-400: oklch(0.70 0.17 60);
  --color-accent-500: oklch(0.63 0.20 60);
  --color-accent-600: oklch(0.54 0.18 60);
  --color-accent-700: oklch(0.44 0.15 60);
  --color-accent-800: oklch(0.34 0.11 60);

  /* Neutral (Gray) — Cool tone */
  --color-neutral-50:  oklch(0.98 0.00 255);
  --color-neutral-100: oklch(0.95 0.01 255);
  --color-neutral-200: oklch(0.88 0.01 255);
  --color-neutral-300: oklch(0.78 0.01 255);
  --color-neutral-400: oklch(0.64 0.01 255);
  --color-neutral-500: oklch(0.52 0.01 255);
  --color-neutral-600: oklch(0.42 0.01 255);
  --color-neutral-700: oklch(0.33 0.01 255);
  --color-neutral-800: oklch(0.23 0.01 255);
  --color-neutral-900: oklch(0.15 0.01 255);
  --color-neutral-950: oklch(0.10 0.01 255);

  /* Semantic colors */
  --color-success: oklch(0.60 0.19 150);
  --color-warning: oklch(0.70 0.17 85);
  --color-error: oklch(0.55 0.22 20);
  --color-info: oklch(0.60 0.16 240);

  /* Surface colors */
  --color-surface: white;
  --color-surface-secondary: oklch(0.97 0.00 255);
  --color-surface-dark: oklch(0.15 0.02 255);
  --color-surface-dark-secondary: oklch(0.20 0.02 255);

  /* === TYPOGRAPHY === */
  --font-sans: "Inter", system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", "Fira Code", monospace;
  --font-display: "Inter", system-ui, sans-serif;

  /* Custom font sizes */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --text-3xl: 1.875rem;
  --text-4xl: 2.25rem;
  --text-5xl: 3rem;
  --text-display: 4rem;
  --text-display--line-height: 1.1;
  --text-display--font-weight: 800;

  /* === SPACING === */
  --spacing-18: 4.5rem;
  --spacing-22: 5.5rem;
  --spacing-30: 7.5rem;
  --spacing-34: 8.5rem;
  --spacing-section: 6rem;
  --spacing-section-lg: 10rem;

  /* === BORDER RADIUS === */
  --radius-xs: 0.25rem;
  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-2xl: 1.5rem;
  --radius-full: 9999px;

  /* === SHADOWS === */
  --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.08), 0 1px 2px -1px rgb(0 0 0 / 0.08);
  --shadow-card-hover: 0 10px 25px -5px rgb(0 0 0 / 0.1), 0 4px 10px -2px rgb(0 0 0 / 0.05);
  --shadow-dropdown: 0 10px 40px -10px rgb(0 0 0 / 0.15);
  --shadow-modal: 0 25px 60px -15px rgb(0 0 0 / 0.25);

  /* === ANIMATIONS === */
  --animate-fade-in: fade-in 0.3s ease-out;
  --animate-slide-up: slide-up 0.4s ease-out;
  --animate-slide-down: slide-down 0.3s ease-out;
  --animate-scale-in: scale-in 0.2s ease-out;

  /* === BREAKPOINTS === */
  --breakpoint-xs: 30rem;
  --breakpoint-sm: 40rem;
  --breakpoint-md: 48rem;
  --breakpoint-lg: 64rem;
  --breakpoint-xl: 80rem;
  --breakpoint-2xl: 96rem;
  --breakpoint-3xl: 120rem;

  /* === CONTAINER === */
  --container-md: 28rem;
  --container-lg: 40rem;
  --container-xl: 56rem;
}

/* === Keyframes === */
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes slide-down {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes scale-in {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}

/* === Base Layer === */
@layer base {
  html {
    scroll-behavior: smooth;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  body {
    font-family: var(--font-sans);
    color: var(--color-neutral-900);
    background-color: var(--color-surface);
  }

  /* Better focus styles */
  :focus-visible {
    outline: 2px solid var(--color-primary-500);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }
}

Giải thích key decisions:

  • Color scale dùng OKLCH với pattern: L giảm dần (0.97 → 0.14), C tăng rồi giảm (tạo độ rực tự nhiên), H giữ nguyên cho consistency.
  • Surface colors: tách biệt surface với brand colors để dễ maintain.
  • --animate-*: Định nghĩa animation trong @theme để dùng với class animate-fade-in, v.v.
  • @layer base: Style toàn cục — smooth scroll, antialiasing, focus ring mặc định.
  • --text-display--line-height: Cú pháp nested cho font size variant — set line-height riêng cho text size cụ thể.

1.6. BÀI TẬP 2 — Extend Default Theme vs Override Entirely

Yêu cầu: So sánh hai approach:

  1. Extend: giữ default Tailwind colors + thêm brand colors
  2. Override: thay thế hoàn toàn default colors

Code đầy đủ:

/* === APPROACH 1: Extend Default Theme === */
@import "tailwindcss";

@theme {
  /* Chỉ thêm brand colors — default colors vẫn giữ nguyên */
  --color-brand: oklch(0.55 0.2 260);
  --color-brand-light: oklch(0.70 0.15 260);
  --color-brand-dark: oklch(0.40 0.15 260);
}

/* Result: Dùng được cả text-brand VÀ text-blue-500, text-red-500... */
/* Extend = default + thêm mới */


/* === APPROACH 2: Override Default Theme === */
@import "tailwindcss";

@theme {
  /* Ghi đè toàn bộ color palette */
  --color-*: initial; /* XÓA tất cả default colors */

  /* Chỉ định nghĩa colors bạn muốn */
  --color-white: #fff;
  --color-black: #000;

  --color-primary-50: oklch(0.97 0.02 260);
  --color-primary-100: oklch(0.92 0.04 260);
  --color-primary-200: oklch(0.84 0.07 260);
  --color-primary-500: oklch(0.55 0.20 260);
  --color-primary-900: oklch(0.20 0.07 260);

  --color-gray-50: oklch(0.98 0.01 260);
  --color-gray-100: oklch(0.95 0.01 260);
  --color-gray-500: oklch(0.52 0.01 260);
  --color-gray-900: oklch(0.15 0.01 260);

  --color-success: oklch(0.60 0.19 150);
  --color-error: oklch(0.55 0.22 20);
}

/* Result: Chỉ dùng được các màu đã định nghĩa */
/* text-red-500 → không tồn tại! */
Khi nào extend, khi nào override?
  • Extend (recommended cho đa số): Bạn muốn đầy đủ color palette + thêm brand colors. Nhanh, an toàn.
  • Override: Design system nghiêm ngặt — bạn muốn giới hạn developer chỉ dùng màu được duyệt. Tốn công hơn nhưng consistency cao hơn.

2. Custom Utilities (@utility)

@utility cho phép bạn tạo custom utility class giống như built-in Tailwind utilities.

2.1. Cú Pháp Cơ Bản

@utility text-balance {
  text-wrap: balance;
}

@utility scrollbar-hide {
  /* Ẩn scrollbar nhưng vẫn scroll được */
  -ms-overflow-style: none;
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}
<!-- Sử dụng như built-in utility -->
<h1 class="text-balance">Tiêu đề dài tự động wrap đẹp từng dòng</h1>
<div class="overflow-auto scrollbar-hide">Nội dung scroll được nhưng không thấy scrollbar</div>

2.2. BÀI TẬP 3 — Custom scrollbar-hide Utility

Code đầy đủ:

/* scrollbar-utilities.css */
@import "tailwindcss";

@utility scrollbar-hide {
  -ms-overflow-style: none;
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

@utility scrollbar-default {
  -ms-overflow-style: auto;
  scrollbar-width: auto;
  &::-webkit-scrollbar {
    display: block;
  }
}

/* Sử dụng trong HTML:
<div class="overflow-auto scrollbar-hide hover:scrollbar-default">
  Component ẩn scrollbar, hiện khi hover
</div>
*/

2.3. BÀI TẬP 4 — Custom text-gradient Utility cho Gradient Text

Code đầy đủ:

/* gradient-utilities.css */
@import "tailwindcss";

@utility text-gradient {
  background: linear-gradient(to right, var(--tw-gradient-stops));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

/* HTML usage:
<h1 class="text-4xl font-bold text-gradient from-blue-500 to-purple-600">
  Gradient Text với Tailwind v4
</h1>

<h1 class="text-4xl font-bold text-gradient from-pink-500 via-red-500 to-yellow-500">
  Three-color gradient text
</h1>
*/
@utility trong v4 có thể dùng & để nhắm vào chính element đang được style, và có thể chứa nested selectors. Điều này mạnh hơn nhiều so với plugin API cũ.

2.4. Utility Có Biến Thể (variants-aware)

@utility content-auto {
  content-visibility: auto;
}

/* Utility này tự động hoạt động với mọi variant: */
/* hover:content-auto, md:content-auto, dark:content-auto */

3. Custom Variants (@custom-variant)

Đã đề cập ở bài Responsive & Dark Mode, nhưng ở đây đi sâu hơn vào kỹ thuật.

3.1. Các Pattern @custom-variant Phổ Biến

/* Pattern 1: Variant dựa trên class cha */
@custom-variant logged-in (&:where(.logged-in, .logged-in *));

/* Pattern 2: Variant dựa trên attribute */
@custom-variant aria-current (&[aria-current="page"]);

/* Pattern 3: Variant dựa trên pseudo-class */
@custom-variant any-pointer (&:any-pointer);

/* Pattern 4: Variant dựa trên media query */
@custom-variant touch (&:where(.touch, .touch *));

/* Pattern 5: Variant cho animation state */
@custom-variant leaving (&.leaving);

3.2. BÀI TẬP 5 — Multi-Theme System với @custom-variant

Yêu cầu: Tạo 4 theme (light, dark, high-contrast, sepia) với custom variants.

Code đầy đủ:

/* multi-theme.css */
@import "tailwindcss";

/* Định nghĩa variants cho từng theme */
@custom-variant theme-dark (&:where(.theme-dark, .theme-dark *));
@custom-variant theme-high-contrast (&:where(.theme-high-contrast, .theme-high-contrast *));
@custom-variant theme-sepia (&:where(.theme-sepia, .theme-sepia *));

/* Có thể dùng trong HTML:
<html class="theme-dark">
  <div class="bg-white theme-dark:bg-gray-900 
              theme-high-contrast:bg-black 
              theme-sepia:bg-amber-50">
    <p class="text-gray-900 theme-dark:text-white 
             theme-high-contrast:text-yellow-300 
             theme-sepia:text-amber-900">
      Nội dung thích nghi theo theme
    </p>
  </div>
</html>
*/

3.3. BÀI TẬP 6 — Custom child:* Variant cho Direct Child Styling

Yêu cầu: Tạo variant cho phép style trực tiếp child elements mà không cần thêm class cho từng child.

Code đầy đủ:

/* child-variant.css */
@import "tailwindcss";

/* Variant nhắm vào tất cả direct children */
@custom-variant children (& > *);

/* Variant nhắm vào direct child cụ thể */
@custom-variant child-first (& > *:first-child);
@custom-variant child-last (& > *:last-child);
@custom-variant child-even (& > *:nth-child(even));
@custom-variant child-odd (& > *:nth-child(odd));

/* Variant nhắm vào children khi parent được hover */
@custom-variant group-children (.group:hover & > *);

/* HTML usage:
<ul class="space-y-2 children:bg-gray-100 children:rounded children:p-3 
           group-children:bg-blue-50 group">
  <li>Item 1</li>
  <li>Item 2 — tất cả tự có bg-gray-100, rounded, p-3</li>
  <li>Item 3 — cả group cùng đổi màu khi hover ul</li>
</ul>

<nav class="flex gap-2 
            children:px-4 children:py-2 children:rounded-lg 
            children:text-sm children:font-medium
            children:transition-colors">
  <a href="#" class="bg-blue-600 text-white">Home</a>
  <a href="#" class="text-gray-600 hover:bg-gray-100">About</a>
  <a href="#" class="text-gray-600 hover:bg-gray-100">Contact</a>
</nav>
*/

4. Functions & Directives

4.1. @import "tailwindcss" — All-in-One

/* v4: một dòng thay thế cả 3 directives của v3 */
@import "tailwindcss";

/* Tương đương với (v3): */
/* @tailwind base; */
/* @tailwind components; */
/* @tailwind utilities; */
/* + tự động expose tất cả tokens thành CSS variables */

4.2. @layer — Tổ Chức CSS

@import "tailwindcss";

/* Layer 1: Base — style toàn cục, reset */
@layer base {
  h1 { font-size: 2rem; }
  h2 { font-size: 1.5rem; }
  a { color: var(--color-primary-600); }
}

/* Layer 2: Components — class tái sử dụng */
@layer components {
  .card {
    background: white;
    border-radius: var(--radius-xl);
    padding: var(--spacing-6);
    box-shadow: var(--shadow-card);
  }

  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-lg);
    font-weight: 500;
    padding: 0.625rem 1.25rem;
    font-size: 0.875rem;
    transition: all 0.15s ease;
    cursor: pointer;
  }
}

/* Layer 3: Utilities — class đơn lẻ (mặc định) */
@layer utilities {
  /* Có thể viết thêm utilities ở đây, */
  /* nhưng @utility (bên ngoài @layer) được khuyến nghị hơn */
}
Thứ tự layer:basecomponentsutilities. Điều này đảm bảo utilities luôn override components, components override base. CSS cascade được quản lý tự động.

4.3. theme() Function

Dùng để truy cập giá trị từ @theme trong CSS:

@import "tailwindcss";

@theme {
  --color-brand: #3b82f6;
  --spacing-gutter: 2rem;
}

.my-component {
  /* Dùng theme() để lấy giá trị từ config */
  color: theme(--color-brand);
  padding: theme(--spacing-gutter);

  /* Có thể dùng với alpha modifier */
  background-color: theme(--color-brand / 20%);
}

/* Hoặc dùng CSS variable trực tiếp (recommended) */
.my-component-2 {
  color: var(--color-brand);
  padding: var(--spacing-gutter);
}

4.4. --value() Helper

Dùng trong @theme để tham chiếu đến giá trị khác:

@theme {
  --color-primary: oklch(0.55 0.2 260);

  /* Dùng --value() để tham chiếu */
  --color-primary-light: --value(--color-primary / 80%);
  /* primary-light = primary với opacity 80% */
}

4.5. --default() (v4.3)

@theme {
  /* Nếu --color-accent chưa được định nghĩa, dùng fallback */
  --color-button-bg: --default(--color-accent, var(--color-primary-500));
}

4.6. BÀI TẬP 7 — Base Styles (Smooth Scroll, Box-Sizing)

Code đầy đủ:

/* base-styles.css */
@import "tailwindcss";

@layer base {
  /* Box-sizing border-box cho tất cả elements */
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  /* Smooth scroll — tôn trọng prefers-reduced-motion */
  html {
    scroll-behavior: smooth;
  }

  @media (prefers-reduced-motion: reduce) {
    html {
      scroll-behavior: auto;
    }
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
    }
  }

  /* Body default */
  body {
    @apply antialiased text-neutral-900 bg-white;
    min-height: 100vh;
    min-height: 100dvh; /* Dynamic viewport height — mobile-friendly */
    line-height: 1.6;
  }

  /* Typography defaults */
  h1, h2, h3, h4, h5, h6 {
    line-height: 1.25;
    font-weight: 600;
  }

  /* Image defaults */
  img, svg, video, canvas {
    display: block;
    max-width: 100%;
  }

  /* Form elements inherit font */
  input, button, textarea, select {
    font: inherit;
  }

  /* Better media defaults */
  img, video {
    height: auto;
  }

  /* Anchor defaults */
  a {
    color: inherit;
    text-decoration: none;
  }

  /* List defaults */
  ul, ol {
    list-style: none;
    padding: 0;
    margin: 0;
  }
}

4.7. BÀI TẬP 8 — Reusable Component Class với @layer components

Yêu cầu: Tạo các component class tái sử dụng: .btn, .card, .input, .badge.

Code đầy đủ:

/* components.css */
@import "tailwindcss";

@theme {
  --color-primary: oklch(0.55 0.2 260);
  --color-primary-hover: oklch(0.46 0.18 260);
  --radius-lg: 0.75rem;
  --shadow-card: 0 1px 3px rgba(0,0,0,0.1);
}

@layer components {
  /* === BUTTON === */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    border-radius: var(--radius-lg);
    font-weight: 600;
    font-size: 0.875rem;
    line-height: 1.25rem;
    padding: 0.625rem 1.25rem;
    transition: all 0.15s ease;
    cursor: pointer;
    border: 1px solid transparent;
    white-space: nowrap;
    user-select: none;
  }

  .btn:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
  }

  .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
  }

  /* Button variants */
  .btn-primary {
    background-color: var(--color-primary);
    color: white;
  }
  .btn-primary:hover {
    background-color: var(--color-primary-hover);
  }

  .btn-secondary {
    background-color: transparent;
    color: var(--color-primary);
    border-color: var(--color-primary);
  }
  .btn-secondary:hover {
    background-color: var(--color-primary);
    color: white;
  }

  .btn-ghost {
    background-color: transparent;
    color: inherit;
  }
  .btn-ghost:hover {
    background-color: rgb(0 0 0 / 0.05);
  }

  /* Button sizes */
  .btn-sm {
    padding: 0.375rem 0.75rem;
    font-size: 0.75rem;
    border-radius: 0.5rem;
  }

  .btn-lg {
    padding: 0.75rem 1.5rem;
    font-size: 1rem;
    border-radius: 1rem;
  }

  .btn-xl {
    padding: 1rem 2rem;
    font-size: 1.125rem;
  }

  /* === CARD === */
  .card {
    background: white;
    border-radius: var(--radius-lg);
    border: 1px solid oklch(0.88 0.01 260);
    overflow: hidden;
  }

  .card-hoverable {
    transition: box-shadow 0.2s ease, transform 0.2s ease;
  }
  .card-hoverable:hover {
    box-shadow: var(--shadow-card);
    transform: translateY(-2px);
  }

  /* Card sections */
  .card-header {
    padding: 1.5rem 1.5rem 0;
  }
  .card-body {
    padding: 1.5rem;
  }
  .card-footer {
    padding: 0 1.5rem 1.5rem;
  }

  /* === INPUT === */
  .input {
    display: block;
    width: 100%;
    padding: 0.625rem 0.875rem;
    font-size: 0.875rem;
    line-height: 1.5;
    color: inherit;
    background-color: white;
    border: 1px solid oklch(0.78 0.01 260);
    border-radius: var(--radius-lg);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
  }

  .input::placeholder {
    color: oklch(0.64 0.01 260);
  }

  .input:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgb(59 130 246 / 0.15);
  }

  .input:disabled {
    background-color: oklch(0.95 0.01 260);
    opacity: 0.7;
    cursor: not-allowed;
  }

  .input-error {
    border-color: oklch(0.55 0.22 20);
  }
  .input-error:focus {
    box-shadow: 0 0 0 3px rgb(239 68 68 / 0.15);
  }

  /* === BADGE === */
  .badge {
    display: inline-flex;
    align-items: center;
    padding: 0.125rem 0.625rem;
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.25rem;
    border-radius: 9999px;
    white-space: nowrap;
  }

  .badge-primary {
    background-color: color-mix(in oklch, var(--color-primary) 15%, transparent);
    color: var(--color-primary);
  }

  .badge-success {
    background-color: color-mix(in oklch, oklch(0.60 0.19 150) 15%, transparent);
    color: oklch(0.45 0.15 150);
  }

  .badge-warning {
    background-color: color-mix(in oklch, oklch(0.70 0.17 85) 15%, transparent);
    color: oklch(0.50 0.13 75);
  }

  .badge-error {
    background-color: color-mix(in oklch, oklch(0.55 0.22 20) 15%, transparent);
    color: oklch(0.45 0.20 20);
  }
}
<!-- HTML Usage Demo -->
<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Component Classes Demo</title>
  <link rel="stylesheet" href="components.css">
</head>
<body class="bg-neutral-50 p-8">

  <div class="max-w-2xl mx-auto space-y-8">

    <!-- Buttons -->
    <section>
      <h2 class="text-xl font-bold mb-4">Buttons</h2>
      <div class="flex flex-wrap gap-3">
        <button class="btn btn-primary">Primary</button>
        <button class="btn btn-secondary">Secondary</button>
        <button class="btn btn-ghost">Ghost</button>
        <button class="btn btn-primary btn-sm">Small</button>
        <button class="btn btn-primary btn-lg">Large</button>
        <button class="btn btn-primary btn-xl">X-Large</button>
        <button class="btn btn-primary" disabled>Disabled</button>
      </div>
    </section>

    <!-- Cards -->
    <section>
      <h2 class="text-xl font-bold mb-4">Cards</h2>
      <div class="grid grid-cols-2 gap-4">
        <div class="card card-hoverable">
          <div class="card-header">
            <h3 class="font-semibold text-lg">Card Title</h3>
          </div>
          <div class="card-body">
            <p class="text-neutral-600">Card content with reusable component classes.</p>
          </div>
          <div class="card-footer">
            <button class="btn btn-ghost btn-sm">Action</button>
          </div>
        </div>

        <div class="card card-hoverable p-6">
          <h3 class="font-semibold text-lg mb-2">Simple Card</h3>
          <p class="text-neutral-600">Dùng card class + padding utility.</p>
        </div>
      </div>
    </section>

    <!-- Inputs -->
    <section>
      <h2 class="text-xl font-bold mb-4">Inputs</h2>
      <div class="space-y-3 max-w-md">
        <div>
          <label class="block text-sm font-medium mb-1">Email</label>
          <input type="email" class="input" placeholder="email@example.com">
        </div>
        <div>
          <label class="block text-sm font-medium mb-1">Error State</label>
          <input type="text" class="input input-error" placeholder="Invalid input" value="bad-value">
          <p class="text-red-600 text-xs mt-1">This field has an error.</p>
        </div>
        <div>
          <label class="block text-sm font-medium mb-1">Disabled</label>
          <input type="text" class="input" placeholder="Cannot edit" disabled>
        </div>
      </div>
    </section>

    <!-- Badges -->
    <section>
      <h2 class="text-xl font-bold mb-4">Badges</h2>
      <div class="flex flex-wrap gap-2">
        <span class="badge badge-primary">Primary</span>
        <span class="badge badge-success">Success</span>
        <span class="badge badge-warning">Warning</span>
        <span class="badge badge-error">Error</span>
      </div>
    </section>

  </div>

</body>
</html>

Giải thích key decisions:

  • @apply không được dùng trong v4 — thay vào đó dùng CSS variable var(--color-primary) để tham chiếu token.
  • color-mix(in oklch, ...) cho badge backgrounds — pha màu với transparency thay vì opacity.
  • :focus-visible thay vì :focus — chỉ hiện focus ring khi keyboard navigation.
  • Các pseudo-class (:hover, :disabled, :focus) định nghĩa trong component class, không cần variant.

5. Preflight (CSS Reset)

Tailwind CSS bao gồm Preflight — một CSS reset hiện đại dựa trên modern-normalize.

5.1. Những gì Preflight Reset

ResetMô tả
Box-sizingbox-sizing: border-box cho tất cả elements
MarginTất cả elements có margin: 0
HeadingHeadings không có font-size, font-weight mặc định
ListLists không có padding, margin, list-style
ImageImages là block-level, max-width 100%
ButtonButtons có cursor pointer mặc định
Borderborder-style: solid mặc định, border-color = currentColor

5.2. Customize Preflight

@import "tailwindcss";

/* Cách 1: Thêm style vào base layer (không xóa preflight) */
@layer base {
  h1 { font-size: 2rem; }
  h2 { font-size: 1.5rem; }
  h3 { font-size: 1.25rem; }
  a { text-decoration: underline; }
}

/* Cách 2: Override specific reset */
@layer base {
  ul, ol {
    list-style: revert; /* Khôi phục bullet points */
    padding: revert;
  }
}

/* Cách 3: Disable preflight hoàn toàn */
/* Thêm vào @theme (CSS-first) hoặc dùng flag */
/* Lưu ý: disable preflight không được khuyến nghị */

6. Design System Workshop

Đây là phần quan trọng nhất của chương này. Xây dựng design system là kỹ năng senior-level. Bạn sẽ tạo một mini design system hoàn chỉnh — từ colors, typography, spacing đến component tokens.

6.1. BÀI TẬP 9 — Define Colors (Primary, Secondary, Neutral, Success, Warning, Error)

@theme {
  /* === PRIMARY (Blue) === */
  --color-primary-50:  oklch(0.97 0.02 255);
  --color-primary-100: oklch(0.92 0.04 255);
  --color-primary-200: oklch(0.84 0.07 255);
  --color-primary-300: oklch(0.74 0.12 255);
  --color-primary-400: oklch(0.64 0.16 255);
  --color-primary-500: oklch(0.54 0.20 255);
  --color-primary-600: oklch(0.46 0.18 255);
  --color-primary-700: oklch(0.37 0.15 255);
  --color-primary-800: oklch(0.28 0.11 255);
  --color-primary-900: oklch(0.20 0.07 255);

  /* === SECONDARY (Violet) === */
  --color-secondary-50:  oklch(0.97 0.02 290);
  --color-secondary-100: oklch(0.92 0.04 290);
  --color-secondary-200: oklch(0.84 0.07 290);
  --color-secondary-300: oklch(0.74 0.12 290);
  --color-secondary-400: oklch(0.64 0.17 290);
  --color-secondary-500: oklch(0.54 0.20 290);
  --color-secondary-600: oklch(0.46 0.18 290);
  --color-secondary-700: oklch(0.37 0.15 290);
  --color-secondary-800: oklch(0.28 0.11 290);
  --color-secondary-900: oklch(0.20 0.07 290);

  /* === NEUTRAL (Gray) === */
  --color-neutral-0:   oklch(1.00 0 0);
  --color-neutral-50:  oklch(0.98 0.01 260);
  --color-neutral-100: oklch(0.95 0.01 260);
  --color-neutral-200: oklch(0.88 0.01 260);
  --color-neutral-300: oklch(0.78 0.01 260);
  --color-neutral-400: oklch(0.64 0.01 260);
  --color-neutral-500: oklch(0.52 0.01 260);
  --color-neutral-600: oklch(0.42 0.01 260);
  --color-neutral-700: oklch(0.33 0.01 260);
  --color-neutral-800: oklch(0.23 0.01 260);
  --color-neutral-900: oklch(0.15 0.01 260);
  --color-neutral-950: oklch(0.10 0.01 260);

  /* === SEMANTIC === */
  --color-success-50:  oklch(0.97 0.02 150);
  --color-success-100: oklch(0.92 0.04 150);
  --color-success-500: oklch(0.60 0.19 150);
  --color-success-600: oklch(0.52 0.18 150);
  --color-success-700: oklch(0.44 0.16 150);

  --color-warning-50:  oklch(0.97 0.02 85);
  --color-warning-100: oklch(0.93 0.04 85);
  --color-warning-500: oklch(0.70 0.17 85);
  --color-warning-600: oklch(0.60 0.15 85);
  --color-warning-700: oklch(0.50 0.13 75);

  --color-error-50:  oklch(0.97 0.02 20);
  --color-error-100: oklch(0.93 0.04 20);
  --color-error-500: oklch(0.55 0.22 20);
  --color-error-600: oklch(0.48 0.21 20);
  --color-error-700: oklch(0.40 0.18 20);

  --color-info-50:  oklch(0.97 0.02 240);
  --color-info-100: oklch(0.92 0.04 240);
  --color-info-500: oklch(0.60 0.16 240);
  --color-info-600: oklch(0.52 0.14 240);
  --color-info-700: oklch(0.44 0.12 240);
}

6.2. BÀI TẬP 10 — Define Typography System

@theme {
  /* Font families */
  --font-sans: "Inter", system-ui, -apple-system, sans-serif;
  --font-serif: "Merriweather", "Georgia", serif;
  --font-mono: "JetBrains Mono", "Fira Code", monospace;

  /* Font sizes — modular scale 1.25 (major third) */
  --text-xs: 0.75rem;    /* 12px */
  --text-sm: 0.875rem;   /* 14px */
  --text-base: 1rem;     /* 16px */
  --text-lg: 1.125rem;   /* 18px */
  --text-xl: 1.25rem;    /* 20px */
  --text-2xl: 1.5rem;    /* 24px */
  --text-3xl: 1.875rem;  /* 30px */
  --text-4xl: 2.25rem;   /* 36px */
  --text-5xl: 3rem;      /* 48px */
  --text-6xl: 3.75rem;   /* 60px */
  --text-7xl: 4.5rem;    /* 72px */

  /* Font weights */
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;
  --font-weight-extrabold: 800;

  /* Line heights */
  --leading-none: 1;
  --leading-tight: 1.25;
  --leading-snug: 1.375;
  --leading-normal: 1.5;
  --leading-relaxed: 1.625;
  --leading-loose: 2;

  /* Letter spacing */
  --tracking-tighter: -0.05em;
  --tracking-tight: -0.025em;
  --tracking-normal: 0;
  --tracking-wide: 0.025em;
  --tracking-wider: 0.05em;

  /* Custom text sizes with built-in line-height */
  --text-display: 5rem;
  --text-display--line-height: 1.1;
  --text-display--font-weight: 800;

  --text-headline: 3rem;
  --text-headline--line-height: 1.2;
  --text-headline--font-weight: 700;

  --text-subheadline: 1.5rem;
  --text-subheadline--line-height: 1.3;
  --text-subheadline--font-weight: 500;
}

6.3. BÀI TẬP 11 — Define Spacing Scale

@theme {
  /* Spacing scale dựa trên 4px grid */
  --spacing-0: 0;
  --spacing-px: 1px;
  --spacing-0_5: 0.125rem;   /* 2px */
  --spacing-1: 0.25rem;      /* 4px */
  --spacing-1_5: 0.375rem;   /* 6px */
  --spacing-2: 0.5rem;       /* 8px */
  --spacing-2_5: 0.625rem;   /* 10px */
  --spacing-3: 0.75rem;      /* 12px */
  --spacing-3_5: 0.875rem;   /* 14px */
  --spacing-4: 1rem;         /* 16px */
  --spacing-5: 1.25rem;      /* 20px */
  --spacing-6: 1.5rem;       /* 24px */
  --spacing-7: 1.75rem;      /* 28px */
  --spacing-8: 2rem;         /* 32px */
  --spacing-9: 2.25rem;      /* 36px */
  --spacing-10: 2.5rem;      /* 40px */
  --spacing-11: 2.75rem;     /* 44px */
  --spacing-12: 3rem;        /* 48px */
  --spacing-14: 3.5rem;      /* 56px */
  --spacing-16: 4rem;        /* 64px */
  --spacing-20: 5rem;        /* 80px */
  --spacing-24: 6rem;        /* 96px */
  --spacing-28: 7rem;        /* 112px */
  --spacing-32: 8rem;        /* 128px */
  --spacing-36: 9rem;        /* 144px */
  --spacing-40: 10rem;       /* 160px */

  /* Semantic spacing tokens */
  --spacing-section: 5rem;
  --spacing-section-sm: 3rem;
  --spacing-section-lg: 8rem;
  --spacing-container-padding: 1.5rem;
  --spacing-card-padding: 1.5rem;
  --spacing-button-padding-x: 1.25rem;
  --spacing-button-padding-y: 0.625rem;
}

6.4. BÀI TẬP 12 — Define Component Tokens (Button, Card, Input)

@theme {
  /* === BUTTON TOKENS === */
  --color-btn-primary-bg: var(--color-primary-500);
  --color-btn-primary-text: white;
  --color-btn-primary-hover-bg: var(--color-primary-600);
  --color-btn-primary-active-bg: var(--color-primary-700);

  --color-btn-secondary-bg: transparent;
  --color-btn-secondary-text: var(--color-primary-500);
  --color-btn-secondary-border: var(--color-primary-500);
  --color-btn-secondary-hover-bg: var(--color-primary-50);

  --color-btn-ghost-bg: transparent;
  --color-btn-ghost-text: var(--color-neutral-700);
  --color-btn-ghost-hover-bg: var(--color-neutral-100);

  /* Button sizing */
  --spacing-btn-sm-px: 0.75rem;
  --spacing-btn-sm-py: 0.375rem;
  --text-btn-sm: 0.75rem;

  --spacing-btn-md-px: 1.25rem;
  --spacing-btn-md-py: 0.625rem;
  --text-btn-md: 0.875rem;

  --spacing-btn-lg-px: 1.5rem;
  --spacing-btn-lg-py: 0.75rem;
  --text-btn-lg: 1rem;

  /* Button shared */
  --radius-btn: var(--radius-lg);
  --font-weight-btn: 600;

  /* === CARD TOKENS === */
  --color-card-bg: white;
  --color-card-border: var(--color-neutral-200);
  --radius-card: var(--radius-xl);
  --shadow-card: 0 1px 3px rgba(0,0,0,0.08);
  --shadow-card-hover: 0 10px 25px -5px rgba(0,0,0,0.1);

  /* === INPUT TOKENS === */
  --color-input-bg: white;
  --color-input-border: var(--color-neutral-300);
  --color-input-text: var(--color-neutral-900);
  --color-input-placeholder: var(--color-neutral-400);
  --color-input-focus-border: var(--color-primary-500);
  --color-input-focus-ring: color-mix(in oklch, var(--color-primary-500) 25%, transparent);
  --color-input-error-border: var(--color-error-500);
  --radius-input: var(--radius-lg);
  --spacing-input-px: 0.875rem;
  --spacing-input-py: 0.625rem;
}

6.5. BÀI TẬP 13 — Full Design System (All Components in One @theme)

Đây là bài tập tổng hợp: gom tất cả tokens từ bài 9-12 vào một file CSS hoàn chỉnh, kèm component classes.

Code đầy đủ:

/* full-design-system.css */
@import "tailwindcss";

/* ============================================================
   DESIGN SYSTEM TOKENS — All in @theme
   ============================================================ */
@theme {
  /* === COLORS === */
  /* Primary */
  --color-primary-50:  oklch(0.97 0.02 255);
  --color-primary-100: oklch(0.92 0.04 255);
  --color-primary-200: oklch(0.84 0.07 255);
  --color-primary-300: oklch(0.74 0.12 255);
  --color-primary-400: oklch(0.64 0.16 255);
  --color-primary-500: oklch(0.54 0.20 255);
  --color-primary-600: oklch(0.46 0.18 255);
  --color-primary-700: oklch(0.37 0.15 255);
  --color-primary-800: oklch(0.28 0.11 255);
  --color-primary-900: oklch(0.20 0.07 255);

  /* Secondary */
  --color-secondary-50:  oklch(0.97 0.02 290);
  --color-secondary-100: oklch(0.92 0.04 290);
  --color-secondary-200: oklch(0.84 0.07 290);
  --color-secondary-300: oklch(0.74 0.12 290);
  --color-secondary-400: oklch(0.64 0.17 290);
  --color-secondary-500: oklch(0.54 0.20 290);
  --color-secondary-600: oklch(0.46 0.18 290);
  --color-secondary-700: oklch(0.37 0.15 290);
  --color-secondary-800: oklch(0.28 0.11 290);
  --color-secondary-900: oklch(0.20 0.07 290);

  /* Neutral */
  --color-neutral-50:  oklch(0.98 0.01 260);
  --color-neutral-100: oklch(0.95 0.01 260);
  --color-neutral-200: oklch(0.88 0.01 260);
  --color-neutral-300: oklch(0.78 0.01 260);
  --color-neutral-400: oklch(0.64 0.01 260);
  --color-neutral-500: oklch(0.52 0.01 260);
  --color-neutral-600: oklch(0.42 0.01 260);
  --color-neutral-700: oklch(0.33 0.01 260);
  --color-neutral-800: oklch(0.23 0.01 260);
  --color-neutral-900: oklch(0.15 0.01 260);
  --color-neutral-950: oklch(0.10 0.01 260);

  /* Semantic */
  --color-success-50:  oklch(0.97 0.02 150);
  --color-success-100: oklch(0.92 0.04 150);
  --color-success-500: oklch(0.60 0.19 150);
  --color-success-600: oklch(0.52 0.18 150);
  --color-success-700: oklch(0.44 0.16 150);

  --color-warning-50:  oklch(0.97 0.02 85);
  --color-warning-100: oklch(0.93 0.04 85);
  --color-warning-500: oklch(0.70 0.17 85);
  --color-warning-600: oklch(0.60 0.15 85);
  --color-warning-700: oklch(0.50 0.13 75);

  --color-error-50:  oklch(0.97 0.02 20);
  --color-error-100: oklch(0.93 0.04 20);
  --color-error-500: oklch(0.55 0.22 20);
  --color-error-600: oklch(0.48 0.21 20);
  --color-error-700: oklch(0.40 0.18 20);

  --color-info-50:  oklch(0.97 0.02 240);
  --color-info-100: oklch(0.92 0.04 240);
  --color-info-500: oklch(0.60 0.16 240);
  --color-info-600: oklch(0.52 0.14 240);
  --color-info-700: oklch(0.44 0.12 240);

  /* Surface */
  --color-surface: white;
  --color-surface-secondary: oklch(0.97 0.01 260);

  /* === TYPOGRAPHY === */
  --font-sans: "Inter", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --text-3xl: 1.875rem;
  --text-4xl: 2.25rem;
  --text-5xl: 3rem;

  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;

  --leading-tight: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.625;

  /* === SPACING === */
  --spacing-section: 5rem;
  --spacing-section-sm: 3rem;
  --spacing-section-lg: 8rem;

  /* === BORDER RADIUS === */
  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-2xl: 1.5rem;
  --radius-full: 9999px;

  /* === SHADOWS === */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.07);
  --shadow-lg: 0 10px 25px -5px rgba(0,0,0,0.1);
  --shadow-xl: 0 20px 50px -10px rgba(0,0,0,0.15);
}

/* ============================================================
   COMPONENT CLASSES
   ============================================================ */
@layer components {
  /* === BUTTON === */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    border-radius: var(--radius-lg);
    font-weight: var(--font-weight-semibold);
    font-size: var(--text-sm);
    line-height: 1.25rem;
    padding: 0.625rem 1.25rem;
    transition: all 0.15s ease;
    cursor: pointer;
    border: 1px solid transparent;
    white-space: nowrap;
  }
  .btn:focus-visible {
    outline: 2px solid var(--color-primary-500);
    outline-offset: 2px;
  }
  .btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Button Variants */
  .btn-primary {
    background-color: var(--color-primary-500);
    color: white;
  }
  .btn-primary:hover {
    background-color: var(--color-primary-600);
  }
  .btn-primary:active {
    background-color: var(--color-primary-700);
  }

  .btn-secondary {
    background-color: transparent;
    color: var(--color-primary-500);
    border-color: var(--color-primary-500);
  }
  .btn-secondary:hover {
    background-color: var(--color-primary-50);
  }

  .btn-ghost {
    background-color: transparent;
    color: var(--color-neutral-700);
  }
  .btn-ghost:hover {
    background-color: var(--color-neutral-100);
  }

  .btn-danger {
    background-color: var(--color-error-500);
    color: white;
  }
  .btn-danger:hover {
    background-color: var(--color-error-600);
  }

  /* Button Sizes */
  .btn-sm {
    padding: 0.375rem 0.75rem;
    font-size: var(--text-xs);
    border-radius: var(--radius-md);
  }
  .btn-lg {
    padding: 0.75rem 1.5rem;
    font-size: var(--text-base);
    border-radius: var(--radius-xl);
  }

  /* === CARD === */
  .card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-neutral-200);
    border-radius: var(--radius-xl);
    overflow: hidden;
  }
  .card-interactive {
    transition: box-shadow 0.2s ease, transform 0.2s ease;
    cursor: pointer;
  }
  .card-interactive:hover {
    box-shadow: var(--shadow-lg);
    transform: translateY(-2px);
  }
  .card-header {
    padding: 1.5rem 1.5rem 0;
  }
  .card-body {
    padding: 1.5rem;
  }
  .card-footer {
    padding: 0 1.5rem 1.5rem;
  }

  /* === INPUT === */
  .input {
    display: block;
    width: 100%;
    padding: 0.625rem 0.875rem;
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--color-neutral-900);
    background-color: white;
    border: 1px solid var(--color-neutral-300);
    border-radius: var(--radius-lg);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
  }
  .input::placeholder {
    color: var(--color-neutral-400);
  }
  .input:focus {
    outline: none;
    border-color: var(--color-primary-500);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary-500) 25%, transparent);
  }
  .input-error {
    border-color: var(--color-error-500);
  }
  .input-error:focus {
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-error-500) 25%, transparent);
  }

  /* === BADGE === */
  .badge {
    display: inline-flex;
    align-items: center;
    padding: 0.125rem 0.75rem;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
    line-height: 1.25rem;
    border-radius: var(--radius-full);
  }
  .badge-primary {
    background-color: color-mix(in oklch, var(--color-primary-500) 15%, transparent);
    color: var(--color-primary-700);
  }
  .badge-success {
    background-color: color-mix(in oklch, var(--color-success-500) 15%, transparent);
    color: var(--color-success-700);
  }
  .badge-warning {
    background-color: color-mix(in oklch, var(--color-warning-500) 15%, transparent);
    color: var(--color-warning-700);
  }
  .badge-error {
    background-color: color-mix(in oklch, var(--color-error-500) 15%, transparent);
    color: var(--color-error-700);
  }
  .badge-info {
    background-color: color-mix(in oklch, var(--color-info-500) 15%, transparent);
    color: var(--color-info-700);
  }
}

/* ============================================================
   BASE STYLES
   ============================================================ */
@layer base {
  html {
    scroll-behavior: smooth;
    -webkit-font-smoothing: antialiased;
  }
  body {
    font-family: var(--font-sans);
    color: var(--color-neutral-900);
    background-color: var(--color-surface);
    min-height: 100dvh;
  }
  :focus-visible {
    outline: 2px solid var(--color-primary-500);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }
}
<!-- HTML Demo cho Design System -->
<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Design System Demo - Tailwind CSS v4</title>
  <link rel="stylesheet" href="full-design-system.css">
</head>
<body class="p-4 md:p-8">

  <div class="max-w-4xl mx-auto space-y-12">

    <!-- Header -->
    <div>
      <h1 class="text-4xl font-bold text-neutral-900 mb-2">Design System Demo</h1>
      <p class="text-neutral-600">Tất cả component được xây dựng từ tokens trong @theme</p>
    </div>

    <!-- Buttons Section -->
    <section>
      <h2 class="text-xl font-semibold text-neutral-900 mb-4 pb-2 border-b border-neutral-200">Buttons</h2>
      
      <div class="space-y-4">
        <!-- Variants -->
        <div>
          <p class="text-sm text-neutral-500 mb-2">Variants</p>
          <div class="flex flex-wrap gap-3">
            <button class="btn btn-primary">Primary</button>
            <button class="btn btn-secondary">Secondary</button>
            <button class="btn btn-ghost">Ghost</button>
            <button class="btn btn-danger">Danger</button>
            <button class="btn btn-primary" disabled>Disabled</button>
          </div>
        </div>

        <!-- Sizes -->
        <div>
          <p class="text-sm text-neutral-500 mb-2">Sizes</p>
          <div class="flex flex-wrap items-center gap-3">
            <button class="btn btn-primary btn-sm">Small</button>
            <button class="btn btn-primary">Default</button>
            <button class="btn btn-primary btn-lg">Large</button>
          </div>
        </div>
      </div>
    </section>

    <!-- Cards Section -->
    <section>
      <h2 class="text-xl font-semibold text-neutral-900 mb-4 pb-2 border-b border-neutral-200">Cards</h2>
      
      <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
        <div class="card card-interactive">
          <div class="card-body">
            <span class="badge badge-primary mb-3">New</span>
            <h3 class="font-semibold text-lg text-neutral-900 mb-1">Basic Card</h3>
            <p class="text-sm text-neutral-600">Simple card with badge and interactive hover.</p>
          </div>
        </div>

        <div class="card">
          <div class="card-header">
            <h3 class="font-semibold text-lg text-neutral-900">Card with Header</h3>
          </div>
          <div class="card-body">
            <p class="text-sm text-neutral-600">Card có header + body + footer rõ ràng.</p>
          </div>
          <div class="card-footer flex justify-end">
            <button class="btn btn-ghost btn-sm">Cancel</button>
            <button class="btn btn-primary btn-sm">Save</button>
          </div>
        </div>

        <div class="card p-6 text-center">
          <div class="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
            <span class="text-primary-600 text-xl"></span>
          </div>
          <h3 class="font-semibold text-neutral-900 mb-1">Success Card</h3>
          <p class="text-sm text-neutral-600">Centered content with icon.</p>
        </div>
      </div>
    </section>

    <!-- Inputs Section -->
    <section>
      <h2 class="text-xl font-semibold text-neutral-900 mb-4 pb-2 border-b border-neutral-200">Inputs</h2>
      
      <div class="max-w-md space-y-4">
        <div>
          <label class="block text-sm font-medium text-neutral-700 mb-1.5">Email address</label>
          <input type="email" class="input" placeholder="you@example.com">
        </div>
        <div>
          <label class="block text-sm font-medium text-neutral-700 mb-1.5">Password</label>
          <input type="password" class="input" placeholder="Enter your password">
        </div>
        <div>
          <label class="block text-sm font-medium text-neutral-700 mb-1.5">Error State</label>
          <input type="text" class="input input-error" value="invalid-value">
          <p class="text-error-600 text-xs mt-1.5">Please enter a valid value.</p>
        </div>
        <div>
          <label class="block text-sm font-medium text-neutral-700 mb-1.5">Disabled</label>
          <input type="text" class="input" placeholder="Cannot edit this field" disabled>
        </div>
      </div>
    </section>

    <!-- Badges Section -->
    <section>
      <h2 class="text-xl font-semibold text-neutral-900 mb-4 pb-2 border-b border-neutral-200">Badges</h2>
      <div class="flex flex-wrap gap-2">
        <span class="badge badge-primary">Primary</span>
        <span class="badge badge-success">Success</span>
        <span class="badge badge-warning">Warning</span>
        <span class="badge badge-error">Error</span>
        <span class="badge badge-info">Info</span>
      </div>
    </section>

    <!-- Color Palette -->
    <section>
      <h2 class="text-xl font-semibold text-neutral-900 mb-4 pb-2 border-b border-neutral-200">Color Palette</h2>
      
      <div class="space-y-4">
        <!-- Primary -->
        <div>
          <p class="text-sm font-medium text-neutral-600 mb-2">Primary</p>
          <div class="flex gap-1">
            <div class="w-10 h-10 rounded bg-primary-50" title="50"></div>
            <div class="w-10 h-10 rounded bg-primary-100" title="100"></div>
            <div class="w-10 h-10 rounded bg-primary-200" title="200"></div>
            <div class="w-10 h-10 rounded bg-primary-300" title="300"></div>
            <div class="w-10 h-10 rounded bg-primary-400" title="400"></div>
            <div class="w-10 h-10 rounded bg-primary-500 ring-2 ring-offset-1 ring-black" title="500 BASE"></div>
            <div class="w-10 h-10 rounded bg-primary-600" title="600"></div>
            <div class="w-10 h-10 rounded bg-primary-700" title="700"></div>
            <div class="w-10 h-10 rounded bg-primary-800" title="800"></div>
            <div class="w-10 h-10 rounded bg-primary-900" title="900"></div>
          </div>
        </div>

        <!-- Neutral -->
        <div>
          <p class="text-sm font-medium text-neutral-600 mb-2">Neutral</p>
          <div class="flex gap-1">
            <div class="w-10 h-10 rounded bg-neutral-50 border border-neutral-200" title="50"></div>
            <div class="w-10 h-10 rounded bg-neutral-100" title="100"></div>
            <div class="w-10 h-10 rounded bg-neutral-200" title="200"></div>
            <div class="w-10 h-10 rounded bg-neutral-300" title="300"></div>
            <div class="w-10 h-10 rounded bg-neutral-400" title="400"></div>
            <div class="w-10 h-10 rounded bg-neutral-500 ring-2 ring-offset-1 ring-black" title="500"></div>
            <div class="w-10 h-10 rounded bg-neutral-600" title="600"></div>
            <div class="w-10 h-10 rounded bg-neutral-700" title="700"></div>
            <div class="w-10 h-10 rounded bg-neutral-800" title="800"></div>
            <div class="w-10 h-10 rounded bg-neutral-900" title="900"></div>
            <div class="w-10 h-10 rounded bg-neutral-950" title="950"></div>
          </div>
        </div>

        <!-- Semantic -->
        <div>
          <p class="text-sm font-medium text-neutral-600 mb-2">Semantic</p>
          <div class="flex gap-3">
            <div class="flex items-center gap-2">
              <div class="w-8 h-8 rounded bg-success-500"></div>
              <span class="text-xs text-neutral-500">Success</span>
            </div>
            <div class="flex items-center gap-2">
              <div class="w-8 h-8 rounded bg-warning-500"></div>
              <span class="text-xs text-neutral-500">Warning</span>
            </div>
            <div class="flex items-center gap-2">
              <div class="w-8 h-8 rounded bg-error-500"></div>
              <span class="text-xs text-neutral-500">Error</span>
            </div>
            <div class="flex items-center gap-2">
              <div class="w-8 h-8 rounded bg-info-500"></div>
              <span class="text-xs text-neutral-500">Info</span>
            </div>
          </div>
        </div>
      </div>
    </section>

  </div>

</body>
</html>

Giải thích key decisions cho toàn bộ design system:

  • Tất cả tokens tập trung trong @theme -> single source of truth.
  • Component classes dùng var(--token-name) để tham chiếu -> đổi token ở một chỗ, tất cả component tự update.
  • color-mix(in oklch, ...) cho badge backgrounds -> giữ nguyên hue, chỉ giảm opacity.
  • :focus-visible thay :focus -> accessibility tốt hơn.
  • 100dvh cho body -> tránh vấn đề 100vh trên mobile browser (address bar ẩn/hiện).

Tổng Kết

CSS-first Config

Bỏ tailwind.config.js. Mọi thứ trong @theme block — colors, fonts, spacing, shadows, animations. Tự động expose thành CSS variables.

OKLCH Colors

Color space mới mặc định. Perceptually uniform, P3-ready. Tạo color scale bằng cách thay đổi Lightness, giữ Chroma và Hue.

@utility & @custom-variant

Tạo utility class và variant selector riêng. @utility hỗ trợ nested selectors với &. Stacked + compound variants từ v4.3.

@layer Architecture

base -> components -> utilities. CSS cascade tự động. @layer components cho reusable classes, @layer base cho global styles.

Component Classes

Dùng CSS variable + pseudo-classes. Không @apply. color-mix() cho transparency. cho accessibility.

Design System

Single @theme chứa tất cả tokens. Component classes tham chiếu tokens qua var(). Đổi token ở 1 chỗ -> toàn bộ system update.
Tiếp theo: 6. Practical Projects — 3 dự án thực tế: Landing Page, Dashboard, E-commerce Product Card.

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.