Customization, Theme & Configuration
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.
"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);
}
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:
| Prefix | Loại token | Ví 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%);
}
/* 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 classanimate-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:
- Extend: giữ default Tailwind colors + thêm brand colors
- 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! */
- 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 */
}
base → components → utilities. Đ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:
@applykhông được dùng trong v4 — thay vào đó dùng CSS variablevar(--color-primary)để tham chiếu token.color-mix(in oklch, ...)cho badge backgrounds — pha màu với transparency thay vì opacity.:focus-visiblethay 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
| Reset | Mô tả |
|---|---|
| Box-sizing | box-sizing: border-box cho tất cả elements |
| Margin | Tất cả elements có margin: 0 |
| Heading | Headings không có font-size, font-weight mặc định |
| List | Lists không có padding, margin, list-style |
| Image | Images là block-level, max-width 100% |
| Button | Buttons có cursor pointer mặc định |
| Border | border-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
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-visiblethay:focus-> accessibility tốt hơn.100dvhcho body -> tránh vấn đề 100vh trên mobile browser (address bar ẩn/hiện).
Tổng Kết
CSS-first Config
tailwind.config.js. Mọi thứ trong @theme block — colors, fonts, spacing, shadows, animations. Tự động expose thành CSS variables.OKLCH Colors
@utility & @custom-variant
@layer Architecture
Component Classes
Design System
4. Responsive & Dark Mode
Tailwind CSS v4 — responsive breakpoints, container queries, dark mode, custom variants. Bài tập responsive layout và dark mode theme.
6. Practical Projects
Áp dụng toàn bộ Tailwind CSS v4 vào 3 dự án thực tế — landing page hoàn chỉnh, dashboard layout, e-commerce product card với interactive states.