Skip to content

📅 NGÀY 8: STYLING TRONG REACT

🎯 Mục tiêu học tập (5 phút)

Sau ngày học hôm nay, bạn sẽ:

  • [ ] Hiểu rõ 4 cách styling trong React và khi nào dùng cách nào
  • [ ] Áp dụng được Inline Styles, CSS Classes, CSS Modules, và Tailwind CSS
  • [ ] Phân biệt được trade-offs giữa các approach
  • [ ] Xây dựng components với styling production-ready

🤔 Kiểm tra đầu vào (5 phút)

Trước khi bắt đầu, hãy trả lời 3 câu hỏi sau:

  1. Conditional Rendering: Làm thế nào để hiển thị element dựa trên condition?
  2. Props: Làm sao truyền data từ parent xuống child component?
  3. Component Composition: Tại sao nên chia nhỏ components?
Xem đáp án
  1. Dùng ternary condition ? <A /> : <B /> hoặc && operator
  2. Truyền qua attributes: <Child name="value" />
  3. Để reusability, maintainability, và single responsibility

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Bạn vừa xây dựng một Product Card component:

jsx
function ProductCard({ name, price, inStock }) {
  return (
    <div>
      <h3>{name}</h3>
      <p>{price}đ</p>
      {inStock ? <span>Còn hàng</span> : <span>Hết hàng</span>}
    </div>
  );
}

Vấn đề: Component này trông rất "xấu" - không màu sắc, không layout, không responsive. Làm sao để style nó?

Câu hỏi đặt ra:

  • Viết CSS ở đâu? Inline hay file riêng?
  • Làm sao tránh CSS conflicts giữa các components?
  • Làm sao style dựa trên props (e.g., inStock → màu xanh/đỏ)?
  • Cách nào performance tốt nhất?

1.2 Giải Pháp

React hỗ trợ 4 cách styling chính, mỗi cách có use case riêng:

Cách tiếp cậnTrường hợp sử dụngƯu điểmNhược điểm
Inline StylesTạo kiểu động dựa trên state/propsLogic JS, an toàn kiểuKhông có lớp giả, dài dòng
CSS ClassesKiểu toàn cục, ứng dụng đơn giảnQuen thuộc, tính năng CSSPhạm vi toàn cục, xung đột
CSS ModulesKiểu giới hạn theo componentCó phạm vi, tính năng CSSThêm file, bước build
Tailwind CSSPhát triển UI nhanhƯu tiên tiện ích, không cần đặt tênLearning curve, HTML rối

1.3 Mô hình tư duy

┌─────────────────────────────────────────────────────┐
│           CÂY QUYẾT ĐỊNH STYLING                    │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Cần style động (màu sắc, kích thước từ props)?     │
│       ├─ CÓ  → Inline Styles                        │
│       └─ KHÔNG → Tiếp tục                           │
│                                                     │
│  Cần tính năng CSS (hover, media queries)?          │
│       ├─ KHÔNG → Inline Styles phù hợp              │
│       └─ CÓ  → Tiếp tục                             │
│                                                     │
│  Muốn style có phạm vi riêng (tránh xung đột)?      │
│       ├─ CÓ  → CSS Modules hoặc Tailwind            │
│       └─ KHÔNG → CSS Classes (toàn cục)             │
│                                                     │
│  Muốn phát triển nhanh với utility classes?         │
│       ├─ CÓ  → Tailwind CSS                         │
│       └─ KHÔNG → CSS Modules                        │
│                                                     │
└─────────────────────────────────────────────────────┘

Analogy dễ hiểu:

Styling trong React giống như trang trí một ngôi nhà:

  • Inline Styles = Sơn trực tiếp lên tường (nhanh nhưng khó maintain)
  • CSS Classes = Dùng giấy dán tường có sẵn (dễ nhưng dễ trùng pattern)
  • CSS Modules = Custom giấy dán riêng cho từng phòng (organized, no conflicts)
  • Tailwind = Lego blocks (ghép nhanh, nhưng phải học cách ghép)

1.4 Hiểu Lầm Phổ Biến

Hiểu lầm 1: "Inline styles là anti-pattern trong React"

  • Sự thật: Inline styles hoàn toàn OK cho dynamic values. Anti-pattern là dùng inline cho STATIC styles.

Hiểu lầm 2: "CSS-in-JS (styled-components) là cách tốt nhất"

  • Sự thật: Styled-components KHÔNG được dạy trong Core React vì cần thư viện external. Chúng ta học CSS Modules + Tailwind trước.

Hiểu lầm 3: "Tailwind làm HTML bẩn"

  • Sự thật: Trade-off giữa "HTML dài" vs "không cần nghĩ tên CSS class". Với components nhỏ, Tailwind rất hiệu quả.

Hiểu lầm 4: "Nên tách hết CSS ra file riêng"

  • Sự thật: Với dynamic styles (dựa vào props), inline styles tốt hơn vì giữ logic gần nhau.

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Inline Styles - Pattern Cơ Bản ⭐

Use case: Button component với dynamic color

❌ Cách SAI - Hard-coded values

jsx
function Button({ children }) {
  return (
    <button
      style={{ backgroundColor: "blue", color: "white", padding: "10px" }}
    >
      {children}
    </button>
  );
}

// Vấn đề:
// 1. Không thể thay đổi màu từ props
// 2. Style object tạo mới mỗi render (performance)
// 3. Không có hover state

✅ Cách ĐÚNG - Dynamic với memoization

jsx
function Button({ children, variant = "primary" }) {
  // Style object ở ngoài để tái sử dụng (hoisted)
  const baseStyle = {
    padding: "10px 20px",
    border: "none",
    borderRadius: "4px",
    cursor: "pointer",
    fontSize: "16px",
  };

  // Dynamic style dựa trên variant
  const variantStyles = {
    primary: { backgroundColor: "#007bff", color: "white" },
    secondary: { backgroundColor: "#6c757d", color: "white" },
    danger: { backgroundColor: "#dc3545", color: "white" },
  };

  return (
    <button style={{ ...baseStyle, ...variantStyles[variant] }}>
      {children}
    </button>
  );
}

// Sử dụng:
// <Button variant="primary">Lưu</Button>
// <Button variant="danger">Xóa</Button>

Tại sao tốt hơn:

  1. ✅ Dynamic color từ props
  2. ✅ Base styles được reuse
  3. ✅ Dễ thêm variants mới
  4. ⚠️ Vẫn thiếu hover state (sẽ fix ở Demo 2)

Demo 2: CSS Classes - Kịch bản Thực Tế ⭐⭐

Use case: Card component với conditional classes

Tạo file Card.css:

css
/* Card.css */
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
  background: white;
  transition: box-shadow 0.3s ease;
}

.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.card--featured {
  border-color: #007bff;
  border-width: 2px;
}

.card__title {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 600;
}

.card__price {
  color: #28a745;
  font-size: 20px;
  font-weight: bold;
}

.card__badge {
  display: inline-block;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 600;
}

.card__badge--in-stock {
  background: #d4edda;
  color: #155724;
}

.card__badge--out-of-stock {
  background: #f8d7da;
  color: #721c24;
}

Component với CSS Classes:

jsx
import "./Card.css";

function ProductCard({ name, price, inStock, featured = false }) {
  // ❌ Cách SAI - String concatenation
  // const cardClass = 'card' + (featured ? ' card--featured' : '');

  // ✅ Cách ĐÚNG - Template literals hoặc array
  const cardClass = `card ${featured ? "card--featured" : ""}`;

  // Hoặc dùng array (dễ đọc hơn với nhiều conditions):
  // const cardClass = ['card', featured && 'card--featured']
  //   .filter(Boolean)
  //   .join(' ');

  const badgeClass = `card__badge ${
    inStock ? "card__badge--in-stock" : "card__badge--out-of-stock"
  }`;

  return (
    <div className={cardClass}>
      <h3 className="card__title">{name}</h3>
      <p className="card__price">{price.toLocaleString("vi-VN")}đ</p>
      <span className={badgeClass}>{inStock ? "Còn hàng" : "Hết hàng"}</span>
    </div>
  );
}

// Kết quả:
// <ProductCard
//   name="iPhone 15 Pro"
//   price={29990000}
//   inStock={true}
//   featured={true}
// />
// → Card có border xanh (featured), badge xanh (còn hàng), hover effect

📚 Vercel Best Practice: Explicit Conditional Rendering

Khi dùng CSS classes với conditions, tránh dùng && với numbers:

jsx
// ❌ SAI - count = 0 sẽ render "0" trong className
const className = `badge ${count && "badge--active"}`;

// ✅ ĐÚNG - Explicit boolean
const className = `badge ${count > 0 ? "badge--active" : ""}`;

Demo 3: CSS Modules - Edge Cases ⭐⭐⭐

Use case: Tránh global CSS conflicts

Vấn đề với CSS thường:

css
/* ComponentA.css */
.button {
  background: blue;
}

/* ComponentB.css */
.button {
  background: red; /* ❌ Conflict! Cái nào load sau sẽ override */
}

Giải pháp: CSS Modules

File Button.module.css:

css
/* Button.module.css */
.button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: all 0.3s ease;
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

.primary {
  background-color: #007bff;
  color: white;
}

.primary:hover {
  background-color: #0056b3;
}

.secondary {
  background-color: #6c757d;
  color: white;
}

.danger {
  background-color: #dc3545;
  color: white;
}

/* Global override (escape hatch) */
:global(.force-large) {
  font-size: 20px !important;
}

Component:

jsx
import styles from "./Button.module.css";

function Button({ children, variant = "primary" }) {
  // CSS Modules import → object với scoped class names
  // styles.button → "Button_button__a1b2c"
  // styles.primary → "Button_primary__d3e4f"

  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}

// Hoặc dùng array method (readable hơn):
function Button({ children, variant = "primary", disabled = false }) {
  const classes = [styles.button, styles[variant], disabled && styles.disabled]
    .filter(Boolean)
    .join(" ");

  return <button className={classes}>{children}</button>;
}

// Kết quả HTML:
// <button class="Button_button__a1b2c Button_primary__d3e4f">
//   Lưu
// </button>

Edge Cases cần handle:

1. Dynamic class names

jsx
// ❌ SAI - Template literal không work với CSS Modules
const className = styles[`button-${variant}`]; // undefined!

// ✅ ĐÚNG - Bracket notation
const className = styles[variant]; // OK nếu variant = 'primary'

// ✅ ĐÚNG - Map object
const variantMap = {
  primary: styles.primary,
  secondary: styles.secondary,
  danger: styles.danger,
};
const className = variantMap[variant];

2. Global styles khi cần

css
/* Button.module.css */

/* Scoped */
.button {
  padding: 10px;
}

/* Global (escape hatch) */
:global(.override-button) {
  padding: 20px;
}
jsx
// Mix scoped + global
<button className={`${styles.button} override-button`}>Click</button>

3. Composition (importing styles từ module khác)

css
/* Base.module.css */
.baseButton {
  padding: 10px;
  border: none;
}

/* PrimaryButton.module.css */
.button {
  composes: baseButton from "./Base.module.css";
  background: blue;
  color: white;
}

Demo 4: Tailwind CSS Basics ⭐⭐⭐

Layout:

  • flex, grid, block, inline, hidden
  • container, mx-auto

Spacing:

  • p-{size}, m-{size} (0, 1, 2, 4, 8, 16...)
  • px-4, py-2, mt-8, mb-4

Colors:

  • bg-{color}-{shade}: bg-blue-500, bg-red-600
  • text-{color}-{shade}: text-gray-700, text-white
  • Colors: blue, red, green, yellow, gray, purple, pink, indigo

Typography:

  • text-{size}: text-sm, text-base, text-lg, text-xl
  • font-{weight}: font-normal, font-semibold, font-bold

Borders:

  • border, border-{width}, border-{color}-{shade}
  • rounded, rounded-lg, rounded-full

Effects:

  • shadow, shadow-md, shadow-lg
  • hover:, focus:, active: prefixes

✅ Component với Tailwind:

jsx
function ProductCard({ name, price, inStock, featured = false }) {
  return (
    <div
      className={`
        border rounded-lg p-4 bg-white
        transition-shadow duration-300
        hover:shadow-lg
        ${featured ? "border-blue-500 border-2" : "border-gray-200"}
      `}
    >
      <h3 className="text-lg font-semibold mb-2">{name}</h3>

      <p className="text-green-600 text-xl font-bold mb-2">
        {price.toLocaleString("vi-VN")}đ
      </p>

      <span
        className={`
          inline-block px-2 py-1 rounded text-xs font-semibold
          ${inStock ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}
        `}
      >
        {inStock ? "Còn hàng" : "Hết hàng"}
      </span>
    </div>
  );
}

// Kết quả: Card responsive, hover effect, conditional styling

❌ Custom tailwind

jsx

// Custom theme config
<div className="bg-brand-primary"> {/* brand-primary không tồn tại */}

// Custom spacing
<div className="p-18"> {/* Chỉ có p-0, p-1, p-2, p-4, p-8, p-16... */}

// Plugins (typography, forms, etc.)
<div className="prose"> {/* @tailwindcss/typography không có */}

// JIT arbitrary values
<div className="p-[13px]"> {/* Arbitrary values không work */}

// ✅ Chỉ dùng core utilities documented
<div className="p-4 bg-blue-500 text-white rounded-lg">

📚 Best Practice: Organize Tailwind Classes

jsx
// ❌ Khó đọc
<div className="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-lg shadow hover:shadow-lg transition-shadow duration-300">

// ✅ Dễ đọc với template literals
<div className={`
  flex items-center justify-between
  p-4 bg-white
  border border-gray-200 rounded-lg
  shadow hover:shadow-lg
  transition-shadow duration-300
`}>

// ✅ Hoặc extract thành constant
const cardClasses = `
  flex items-center justify-between
  p-4 bg-white
  border border-gray-200 rounded-lg
  shadow hover:shadow-lg
  transition-shadow duration-300
`;

<div className={cardClasses}>

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Exercise 1: Warmup - Inline Dynamic Styles (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Practice inline styles với dynamic values
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: useEffect, useMemo, CSS files
 *
 * Requirements:
 * 1. Tạo ProgressBar component nhận `percent` prop (0-100)
 * 2. Bar width thay đổi theo percent
 * 3. Color:
 *    - 0-30%: red
 *    - 31-70%: yellow
 *    - 71-100%: green
 * 4. Smooth transition
 *
 * 💡 Gợi ý: Dùng inline styles cho width và backgroundColor
 */

// ❌ Cách SAI (Anti-pattern):
function ProgressBar({ percent }) {
  // Style object recreated mỗi render
  return (
    <div style={{ background: "#eee", height: 20 }}>
      <div
        style={{
          width: `${percent}%`,
          height: "100%",
          background: percent > 70 ? "green" : percent > 30 ? "yellow" : "red",
          transition: "width 0.3s ease",
        }}
      />
    </div>
  );
}

// ✅ Cách ĐÚNG (Best practice):
// TODO: Implement đúng với style hoisting

// 🎯 NHIỆM VỤ CỦA BẠN:
function ProgressBar({ percent }) {
  // TODO:
  // 1. Hoist static styles
  // 2. Calculate dynamic color
  // 3. Return JSX với đúng styles
}

// Test cases:
// <ProgressBar percent={25} />  → red bar, 25% width
// <ProgressBar percent={50} />  → yellow bar, 50% width
// <ProgressBar percent={90} />  → green bar, 90% width
💡 Solution
jsx
/**
 * ProgressBar Component
 *
 * @param {Object} props
 * @param {number} props.percent - Progress percentage (0-100)
 * @returns {JSX.Element}
 */
function ProgressBar({ percent }) {
  // Hoist static styles ra ngoài để tránh recreate mỗi render
  const containerStyle = {
    width: "100%",
    height: "20px",
    backgroundColor: "#eee",
    borderRadius: "4px",
    overflow: "hidden",
  };

  // Helper function: Tính màu dựa trên percent
  const getColor = (value) => {
    if (value <= 30) return "#dc3545"; // red
    if (value <= 70) return "#ffc107"; // yellow
    return "#28a745"; // green
  };

  // Dynamic styles cho bar
  const barStyle = {
    width: `${percent}%`,
    height: "100%",
    backgroundColor: getColor(percent),
    transition: "width 0.3s ease, background-color 0.3s ease",
  };

  return (
    <div style={containerStyle}>
      <div style={barStyle} />
    </div>
  );
}

// Test cases:
// <ProgressBar percent={25} />  → red bar, 25% width
// <ProgressBar percent={50} />  → yellow bar, 50% width
// <ProgressBar percent={90} />  → green bar, 90% width
md
## 📊 Giải thích

### ✅ Điểm chính:

1. **Static styles được hoist:** `containerStyle` ở ngoài JSX
2. **Dynamic calculation:** `getColor()` function tính màu
3. **Smooth transition:** `transition` CSS property
4. **Template literals:** `${percent}%` cho width động

### ✅ Tại sao tốt:

- ✅ Base styles không recreate mỗi render
- ✅ Dynamic values (width, color) được tính đúng
- ✅ Smooth animation khi percent thay đổi
- ✅ Clean, readable code

⭐⭐ Exercise 2: Pattern Recognition - CSS vs Tailwind (25 phút)

jsx
/**
 * 🎯 Mục tiêu: So sánh và chọn approach phù hợp
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Bạn đang build Alert component với 3 variants
 *
 * 🤔 PHÂN TÍCH:
 * Approach A: CSS Classes (global)
 * Pros: Familiar, full CSS power
 * Cons: Potential conflicts, need naming
 *
 * Approach B: CSS Modules
 * Pros: Scoped, full CSS power
 * Cons: Extra file, mapping syntax
 *
 * Approach C: Tailwind
 * Pros: Fast, no naming, utility-first
 * Cons: Long className, learning curve
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 * Write your decision in comments, then implement
 */

// Design spec:
/**
 * Alert variants:
 * - success: green bg, dark green text, checkmark icon
 * - error: red bg, dark red text, X icon
 * - warning: yellow bg, dark yellow text, ! icon
 *
 * Common:
 * - Rounded corners
 * - Padding
 * - Border left (thick, variant color)
 */

// TODO: Implement bằng approach bạn chọn
function Alert({ variant, children }) {
  // Your implementation
}

// Test:
// <Alert variant="success">Saved successfully!</Alert>
// <Alert variant="error">Something went wrong</Alert>
// <Alert variant="warning">Please review</Alert>
💡 Solution - Approach A: CSS Classes
jsx
/**
 * Alert Component - CSS Classes Approach
 *
 * Decision: CSS Classes
 * Rationale:
 * - Simple component, unlikely to have naming conflicts
 * - Team familiar with traditional CSS
 * - Full control over hover states and animations
 *
 * @param {Object} props
 * @param {('success'|'error'|'warning')} props.variant - Alert type
 * @param {React.ReactNode} props.children - Alert content
 * @returns {JSX.Element}
 */

// Alert.css
/*
.alert {
  padding: 12px 16px;
  border-radius: 4px;
  border-left: 4px solid;
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  line-height: 1.5;
}

.alert__icon {
  flex-shrink: 0;
  font-size: 18px;
  font-weight: bold;
}

.alert--success {
  background-color: #d4edda;
  color: #155724;
  border-left-color: #28a745;
}

.alert--error {
  background-color: #f8d7da;
  color: #721c24;
  border-left-color: #dc3545;
}

.alert--warning {
  background-color: #fff3cd;
  color: #856404;
  border-left-color: #ffc107;
}
*/

// Alert.jsx
import "./Alert.css";

function Alert({ variant, children }) {
  // Icon mapping
  const icons = {
    success: "✓",
    error: "✕",
    warning: "!",
  };

  // Build className với template literal
  const alertClass = `alert alert--${variant}`;

  return (
    <div className={alertClass}>
      <span className="alert__icon">{icons[variant]}</span>
      <div>{children}</div>
    </div>
  );
}

// Test cases:
// <Alert variant="success">Saved successfully!</Alert>
// <Alert variant="error">Something went wrong</Alert>
// <Alert variant="warning">Please review</Alert>

💡 Solution - Approach B: CSS Modules
jsx
/**
 * Alert Component - CSS Modules Approach
 *
 * Decision: CSS Modules
 * Rationale:
 * - Component library với nhiều components → tránh conflicts
 * - Scoped styles, không ảnh hưởng global
 * - Vẫn giữ được full CSS power
 *
 * @param {Object} props
 * @param {('success'|'error'|'warning')} props.variant - Alert type
 * @param {React.ReactNode} props.children - Alert content
 * @returns {JSX.Element}
 */

// Alert.module.css
/*
.alert {
  padding: 12px 16px;
  border-radius: 4px;
  border-left: 4px solid;
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  line-height: 1.5;
}

.icon {
  flex-shrink: 0;
  font-size: 18px;
  font-weight: bold;
}

.success {
  background-color: #d4edda;
  color: #155724;
  border-left-color: #28a745;
}

.error {
  background-color: #f8d7da;
  color: #721c24;
  border-left-color: #dc3545;
}

.warning {
  background-color: #fff3cd;
  color: #856404;
  border-left-color: #ffc107;
}
*/

// Alert.jsx
import styles from "./Alert.module.css";

function Alert({ variant, children }) {
  // Icon mapping
  const icons = {
    success: "✓",
    error: "✕",
    warning: "!",
  };

  // Combine base + variant classes
  // styles.alert → "Alert_alert__x1y2z"
  // styles[variant] → "Alert_success__a3b4c"
  const alertClass = `${styles.alert} ${styles[variant]}`;

  return (
    <div className={alertClass}>
      <span className={styles.icon}>{icons[variant]}</span>
      <div>{children}</div>
    </div>
  );
}

// Test cases:
// <Alert variant="success">Saved successfully!</Alert>
// <Alert variant="error">Something went wrong</Alert>
// <Alert variant="warning">Please review</Alert>

💡 Solution - Approach C: Tailwind CSS
jsx
/**
 * Alert Component - Tailwind Approach
 *
 * Decision: Tailwind CSS
 * Rationale:
 * - Rapid development, no need to name CSS classes
 * - Design system constraints (spacing, colors) enforced
 * - Component co-located với styles (no separate file)
 *
 * @param {Object} props
 * @param {('success'|'error'|'warning')} props.variant - Alert type
 * @param {React.ReactNode} props.children - Alert content
 * @returns {JSX.Element}
 */
function Alert({ variant, children }) {
  // Icon mapping
  const icons = {
    success: "✓",
    error: "✕",
    warning: "!",
  };

  // Variant styles mapping
  const variantStyles = {
    success: "bg-green-100 text-green-800 border-green-500",
    error: "bg-red-100 text-red-800 border-red-500",
    warning: "bg-yellow-100 text-yellow-800 border-yellow-500",
  };

  return (
    <div
      className={`
        flex items-center gap-3
        p-3 rounded
        border-l-4
        text-sm
        ${variantStyles[variant]}
      `}
    >
      <span className="flex-shrink-0 text-lg font-bold">{icons[variant]}</span>
      <div>{children}</div>
    </div>
  );
}

// Test cases:
// <Alert variant="success">Saved successfully!</Alert>
// <Alert variant="error">Something went wrong</Alert>
// <Alert variant="warning">Please review</Alert>

📊 So sánh 3 Approaches
md
### Decision Matrix:

| Criteria            | CSS Classes      | CSS Modules          | Tailwind       |
| ------------------- | ---------------- | -------------------- | -------------- |
| **Setup time**      | 5 min (CSS file) | 7 min (CSS + config) | 2 min (inline) |
| **Readability**     | ⭐⭐⭐⭐⭐       | ⭐⭐⭐⭐             | ⭐⭐⭐         |
| **Maintainability** | ⭐⭐⭐           | ⭐⭐⭐⭐⭐           | ⭐⭐⭐⭐       |
| **No conflicts**    | ❌               | ✅                   | ✅             |
| **Team learning**   | None             | Minimal              | Moderate       |

### ✅ Khi nào dùng cái nào:

**CSS Classes:**

- ✅ Small project (<10 components)
- ✅ Team quen CSS truyền thống
- ✅ Cần full CSS control (complex animations)

**CSS Modules:**

- ✅ Medium/Large project
- ✅ Component library
- ✅ Cần scoping + full CSS power

**Tailwind:**

- ✅ Rapid prototyping
- ✅ Design system có sẵn
- ✅ Team prefer utility-first

⭐⭐⭐ Exercise 3: Real Scenario - Card Grid (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Build production-ready component
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là user, tôi muốn xem danh sách sản phẩm dạng grid
 *              với hover effects và responsive layout"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Grid: 3 columns desktop, 2 tablet, 1 mobile
 * - [ ] Card hover: lift up + shadow
 * - [ ] Image: rounded, aspect ratio 1:1
 * - [ ] Badge "Sale" nếu có discount
 * - [ ] Button disabled nếu out of stock
 *
 * 🎨 Technical Constraints:
 * - Dùng CSS Modules hoặc Tailwind (chọn 1)
 * - Không dùng external libraries
 * - Code phải clean, readable
 *
 * 🚨 Edge Cases cần handle:
 * - Long product names (truncate)
 * - Missing images (placeholder)
 * - Price = 0 (show "Contact")
 * - Discount > 50% (badge "Hot Deal")
 */

const products = [
  {
    id: 1,
    name: "iPhone 15 Pro Max",
    price: 29990000,
    image: "/iphone.jpg",
    inStock: true,
    discount: 0,
  },
  {
    id: 2,
    name: "Samsung Galaxy S24 Ultra",
    price: 27990000,
    image: "/samsung.jpg",
    inStock: true,
    discount: 15,
  },
  {
    id: 3,
    name: 'MacBook Pro 16"',
    price: 0, // Contact for price
    image: "/macbook.jpg",
    inStock: false,
    discount: 0,
  },
];

// 📝 Implementation Checklist:
// - [ ] ProductGrid component (grid layout)
// - [ ] ProductCard component (individual card)
// - [ ] Responsive (media queries hoặc Tailwind md:, lg:)
// - [ ] Hover states
// - [ ] Conditional rendering (badge, button state)
// - [ ] Edge cases handled

function ProductGrid({ products }) {
  // TODO: Implement
}

function ProductCard({ product }) {
  // TODO: Implement
}

// Expected output:
/**
 * Desktop (3 cols):
 * ┌──────┐ ┌──────┐ ┌──────┐
 * │ Card │ │ Card │ │ Card │
 * └──────┘ └──────┘ └──────┘
 *
 * Tablet (2 cols):
 * ┌──────┐ ┌──────┐
 * │ Card │ │ Card │
 * └──────┘ └──────┘
 *
 * Mobile (1 col):
 * ┌──────┐
 * │ Card │
 * └──────┘
 */
💡 Solution - Approach A: CSS Modules
jsx
/**
 * ProductGrid Component - CSS Modules Approach
 *
 * @param {Object} props
 * @param {Array} props.products - Array of product objects
 * @returns {JSX.Element}
 */

// ProductGrid.module.css
/*
.grid {
  display: grid;
  gap: 24px;
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

/* Mobile: 1 column */
@media (min-width: 0) {
  .grid {
    grid-template-columns: 1fr;
  }
}

/* Tablet: 2 columns */
@media (min-width: 768px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop: 3 columns */
@media (min-width: 1024px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.card {
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  cursor: pointer;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}

.imageContainer {
  position: relative;
  width: 100%;
  padding-top: 100%; /* 1:1 aspect ratio */
  background: #f5f5f5;
  overflow: hidden;
}

.image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.imagePlaceholder {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #e0e0e0;
  color: #999;
  font-size: 48px;
}

.badge {
  position: absolute;
  top: 12px;
  right: 12px;
  padding: 4px 12px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 600;
  color: white;
}

.badgeSale {
  background: #ff6b6b;
}

.badgeHotDeal {
  background: #ff3838;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
}

.content {
  padding: 16px;
}

.name {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 8px 0;
  color: #333;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.priceContainer {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.price {
  font-size: 20px;
  font-weight: bold;
  color: #28a745;
}

.priceOriginal {
  font-size: 14px;
  color: #999;
  text-decoration: line-through;
}

.contact {
  font-size: 14px;
  color: #007bff;
  font-weight: 600;
}

.button {
  width: 100%;
  padding: 10px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.buttonPrimary {
  background: #007bff;
  color: white;
}

.buttonPrimary:hover {
  background: #0056b3;
}

.buttonDisabled {
  background: #e0e0e0;
  color: #999;
  cursor: not-allowed;
}
*/

import styles from './ProductGrid.module.css';

function ProductGrid({ products }) {
  return (
    <div className={styles.grid}>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

function ProductCard({ product }) {
  const { name, price, image, inStock, discount } = product;

  // Calculate discounted price
  const discountedPrice = discount > 0
    ? price - (price * discount / 100)
    : price;

  // Handle image error
  const handleImageError = (e) => {
    e.target.style.display = 'none';
    e.target.nextSibling.style.display = 'flex';
  };

  return (
    <div className={styles.card}>
      {/* Image */}
      <div className={styles.imageContainer}>
        {image ? (
          <>
            <img
              src={image}
              alt={name}
              className={styles.image}
              onError={handleImageError}
            />
            <div
              className={styles.imagePlaceholder}
              style={{ display: 'none' }}
            >
              📷
            </div>
          </>
        ) : (
          <div className={styles.imagePlaceholder}>📷</div>
        )}

        {/* Badge */}
        {discount > 0 && (
          <span
            className={`${styles.badge} ${
              discount > 50 ? styles.badgeHotDeal : styles.badgeSale
            }`}
          >
            {discount > 50 ? '🔥 Hot Deal' : `Sale ${discount}%`}
          </span>
        )}
      </div>

      {/* Content */}
      <div className={styles.content}>
        <h3 className={styles.name} title={name}>
          {name}
        </h3>

        {/* Price */}
        <div className={styles.priceContainer}>
          {price === 0 ? (
            <span className={styles.contact}>Liên hệ để biết giá</span>
          ) : (
            <>
              <span className={styles.price}>
                {discountedPrice.toLocaleString('vi-VN')}đ
              </span>
              {discount > 0 && (
                <span className={styles.priceOriginal}>
                  {price.toLocaleString('vi-VN')}đ
                </span>
              )}
            </>
          )}
        </div>

        {/* Button */}
        <button
          className={`${styles.button} ${
            inStock ? styles.buttonPrimary : styles.buttonDisabled
          }`}
          disabled={!inStock}
        >
          {inStock ? 'Thêm vào giỏ' : 'Hết hàng'}
        </button>
      </div>
    </div>
  );
}

// Export
// <ProductGrid products={products} />

💡 Solution - Approach B: Tailwind CSS
jsx
/**
 * ProductGrid Component - Tailwind Approach
 *
 * @param {Object} props
 * @param {Array} props.products - Array of product objects
 * @returns {JSX.Element}
 */
function ProductGrid({ products }) {
  return (
    <div
      className="
      grid gap-6 p-5
      grid-cols-1
      md:grid-cols-2
      lg:grid-cols-3
      max-w-6xl mx-auto
    "
    >
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

function ProductCard({ product }) {
  const { name, price, image, inStock, discount } = product;

  // Calculate discounted price
  const discountedPrice =
    discount > 0 ? price - (price * discount) / 100 : price;

  // Handle image error
  const handleImageError = (e) => {
    e.target.style.display = "none";
    e.target.nextSibling.style.display = "flex";
  };

  return (
    <div
      className="
      bg-white border border-gray-200 rounded-lg overflow-hidden
      transition-all duration-300
      hover:-translate-y-1 hover:shadow-lg
      cursor-pointer
    "
    >
      {/* Image Container */}
      <div className="relative w-full pt-[100%] bg-gray-100 overflow-hidden">
        {image ? (
          <>
            <img
              src={image}
              alt={name}
              className="absolute top-0 left-0 w-full h-full object-cover"
              onError={handleImageError}
            />
            <div className="absolute top-0 left-0 w-full h-full hidden items-center justify-center bg-gray-200 text-gray-400 text-5xl">
              📷
            </div>
          </>
        ) : (
          <div className="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-gray-200 text-gray-400 text-5xl">
            📷
          </div>
        )}

        {/* Badge */}
        {discount > 0 && (
          <span
            className={`
            absolute top-3 right-3
            px-3 py-1 rounded
            text-xs font-semibold text-white
            ${discount > 50 ? "bg-red-600 animate-pulse" : "bg-red-400"}
          `}
          >
            {discount > 50 ? "🔥 Hot Deal" : `Sale ${discount}%`}
          </span>
        )}
      </div>

      {/* Content */}
      <div className="p-4">
        {/* Product Name */}
        <h3
          className="text-base font-semibold mb-2 text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap"
          title={name}
        >
          {name}
        </h3>

        {/* Price */}
        <div className="flex items-center gap-2 mb-3">
          {price === 0 ? (
            <span className="text-sm text-blue-600 font-semibold">
              Liên hệ để biết giá
            </span>
          ) : (
            <>
              <span className="text-xl font-bold text-green-600">
                {discountedPrice.toLocaleString("vi-VN")}đ
              </span>
              {discount > 0 && (
                <span className="text-sm text-gray-400 line-through">
                  {price.toLocaleString("vi-VN")}đ
                </span>
              )}
            </>
          )}
        </div>

        {/* Button */}
        <button
          className={`
            w-full py-2 px-4 rounded
            text-sm font-semibold
            transition-colors duration-300
            ${
              inStock
                ? "bg-blue-600 text-white hover:bg-blue-700 cursor-pointer"
                : "bg-gray-200 text-gray-400 cursor-not-allowed"
            }
          `}
          disabled={!inStock}
        >
          {inStock ? "Thêm vào giỏ" : "Hết hàng"}
        </button>
      </div>
    </div>
  );
}

📊 Giải thích
md
### ✅ Acceptance Criteria đã đáp ứng:

1. **Grid responsive:**
   - CSS Modules: `@media` queries
   - Tailwind: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`

2. **Card hover effects:**
   - Lift up: `transform: translateY(-4px)` / `hover:-translate-y-1`
   - Shadow: `box-shadow` / `hover:shadow-lg`

3. **Image aspect ratio 1:1:**
   - `padding-top: 100%` trick / `pt-[100%]`
   - Absolute positioned image inside

4. **Badge "Sale":**
   - Conditional: `discount > 0`
   - Hot Deal: `discount > 50` → pulse animation

5. **Button disabled:**
   - `disabled={!inStock}`
   - Different styles cho disabled state

### ✅ Edge Cases đã handle:

1. **Long product names:**
   - `text-overflow: ellipsis` + `white-space: nowrap`
   - `title` attribute để show full name on hover

2. **Missing images:**
   - Placeholder với icon 📷
   - `onError` handler để fallback

3. **Price = 0:**
   - Show "Liên hệ để biết giá"
   - Conditional rendering

4. **Discount > 50%:**
   - Badge "🔥 Hot Deal"
   - Pulse animation (CSS Modules) / `animate-pulse` (Tailwind)

### ✅ Production Quality:

- ✅ Clean component structure (Grid + Card separated)
- ✅ Responsive design (mobile-first)
- ✅ Smooth transitions
- ✅ Accessibility (alt text, title, disabled state)
- ✅ Vietnamese locale formatting

⭐⭐⭐⭐ Exercise 4: Architecture Decision - Style System (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Đưa ra quyết định kiến trúc có lý do
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Scenario: Bạn là tech lead của startup, cần chọn styling approach
 * cho React app mới (dashboard admin với ~50 components)
 *
 * Nhiệm vụ:
 * 1. So sánh ít nhất 3 approaches (CSS Modules, Tailwind, Styled-components)
 * 2. Document pros/cons mỗi approach
 * 3. Chọn approach phù hợp nhất
 * 4. Viết ADR (Architecture Decision Record)
 *
 * ADR Template:
 * - Context: Team size, timeline, skill level
 * - Decision: Approach đã chọn
 * - Rationale: Tại sao (ít nhất 3 reasons)
 * - Consequences: Trade-offs accepted
 * - Alternatives Considered: Các options khác và tại sao không chọn
 *
 * 💻 PHASE 2: Implementation (30 phút)
 *
 * Implement 1 component phức tạp với approach đã chọn:
 * - Sidebar navigation (collapsible)
 * - Active state
 * - Hover effects
 * - Responsive
 * - Theme support (light/dark)
 */

// ADR Template
/**
 * # ADR: Styling Approach for Admin Dashboard
 *
 * ## Context
 * - Team: [size, skill level]
 * - Timeline: [deadline]
 * - Requirements: [key features]
 *
 * ## Decision
 * We will use [APPROACH] for styling.
 *
 * ## Rationale
 * 1. [Reason 1]
 * 2. [Reason 2]
 * 3. [Reason 3]
 *
 * ## Consequences
 * ### Positive
 * - [Benefit 1]
 * - [Benefit 2]
 *
 * ### Negative
 * - [Trade-off 1]
 * - [Trade-off 2]
 *
 * ## Alternatives Considered
 * ### [Approach A]
 * Rejected because: [reason]
 *
 * ### [Approach B]
 * Rejected because: [reason]
 */

// TODO: Write your ADR above

// 🧪 PHASE 3: Proof of Concept (10 phút)
// Implement Sidebar với approach đã chọn

function Sidebar({ isCollapsed, theme }) {
  // TODO: Implement với approach bạn chọn
  // Requirements:
  // - Collapsible (width changes)
  // - Menu items with icons
  // - Active state highlighting
  // - Hover effects
  // - Light/dark theme support
}

// Expected behavior:
// isCollapsed=false → Full width (240px), show text + icons
// isCollapsed=true  → Narrow (60px), show icons only
// theme="dark"      → Dark background, light text
// theme="light"     → Light background, dark text
💡 Solution
markdown
# ADR: Styling Approach for Admin Dashboard

## Context

- **Team**: 5 developers (2 senior, 3 mid/junior), frontend-focused startup
- **Timeline**: MVP cần ra mắt trong 10–12 tuần, sau đó iterate nhanh (2 tuần/sprint)
- **Skill level**: Team đã có kinh nghiệm với Tailwind (3/5 người dùng thường xuyên), 2 người mới nhưng học nhanh, mọi người đều quen CSS Modules cơ bản
- **Requirements chính**:
  - ~50–70 components (dashboard admin: sidebar, table, forms, charts, modals, cards…)
  - Responsive (desktop + tablet, không cần mobile-first nghiêm ngặt)
  - Hỗ trợ light/dark theme (khách hàng yêu cầu)
  - Tốc độ phát triển UI là ưu tiên hàng đầu
  - Muốn giảm thiểu thời gian debug CSS conflict & naming class
  - Không muốn thêm runtime overhead lớn (như styled-components)

## Decision

We will use **Tailwind CSS** (utility-first) làm styling approach chính cho toàn bộ admin dashboard.

## Rationale

1. **Tốc độ phát triển UI nhanh nhất** — Không cần tạo file CSS riêng, không cần đặt tên class, mọi style nằm ngay trong JSX → phù hợp timeline gấp và startup cần iterate nhanh.
2. **Responsive & dark mode tích hợp cực kỳ tiện lợi**`md:`, `lg:`, `dark:` prefix giúp xử lý responsive và theme chỉ trong 1 dòng class → giảm ~40–50% thời gian viết style so với CSS Modules.
3. **Không còn vấn đề global namespace & class naming hell** — Utility classes loại bỏ hoàn toàn việc tranh cãi tên class, override không mong muốn, hoặc debug specificity.
4. **Consistency cao nhờ design tokens** — Tailwind config (spacing, colors, shadows…) ép team dùng scale thống nhất → giảm "magic numbers" và design drift.
5. **Onboarding nhanh cho junior** — Chỉ cần học ~100–150 utility phổ biến là code được UI đẹp, không cần hiểu sâu CSS cascade/specificity ngay từ đầu.

## Consequences

### Positive

- Phát triển giao diện nhanh gấp 2–3 lần so với CSS Modules hoặc styled-components
- Dark mode gần như miễn phí (chỉ thêm `dark:` prefix)
- Dễ refactor khi design system thay đổi (chỉ tìm & thay class)
- Bundle size nhỏ nhờ PurgeCSS/JIT mode
- Team có thể tập trung vào logic & state thay vì CSS maintainability

### Negative (trade-offs accepted)

- ClassName dài, JSX trông "bẩn" hơn (đặc biệt với component phức tạp)
- Learning curve ban đầu cho 2 junior chưa dùng Tailwind (ước tính 1–2 tuần)
- Khó viết animation/custom transition phức tạp (cần custom CSS hoặc plugin)
- Phụ thuộc vào Tailwind config (nhưng có thể customize thoải mái)
- Không lý tưởng nếu sau này cần pixel-perfect design từ designer (nhưng dashboard admin thường không yêu cầu mức đó)

## Alternatives Considered

### CSS Modules + CSS Variables

Rejected because:

- Phải tạo file .module.css riêng cho mỗi component → chuyển tab liên tục, chậm phát triển
- Cần đặt tên class có ý nghĩa → tốn thời gian & dễ gây tranh cãi trong team
- Dark mode phải viết lại selector phức tạp (hoặc dùng nhiều class)
- Responsive cần viết media query thủ công → code dài hơn nhiều so với Tailwind breakpoints

### Styled-components / Emotion

Rejected because:

- Thêm runtime overhead (không lý tưởng cho dashboard có nhiều table/chart)
- Bundle size lớn hơn Tailwind + PurgeCSS (đặc biệt khi có nhiều component)
- Cần học cú pháp mới (styled.div`...`) → tăng cognitive load cho junior
- Theme/dark mode phức tạp hơn (cần ThemeProvider + nhiều boilerplate)
- Không phù hợp khi ưu tiên tốc độ phát triển & stack đơn giản

=> **Tailwind là lựa chọn cân bằng tốt nhất** giữa tốc độ, maintainability, consistency và khả năng mở rộng cho dự án admin dashboard này.

PHASE 3: Proof of Concept – Sidebar với Tailwind

jsx
/**
 * Sidebar - Thanh điều hướng collapsible, hỗ trợ light/dark theme
 * @param {boolean} isCollapsed - Trạng thái thu gọn sidebar
 * @param {'light' | 'dark'} [theme='light'] - Theme hiện tại
 */
function Sidebar({ isCollapsed = false, theme = "light" }) {
  const isDark = theme === "dark";

  const menuItems = [
    { icon: "🏠", label: "Dashboard", active: true },
    { icon: "👤", label: "Users", active: false },
    { icon: "📦", label: "Products", active: false },
    { icon: "📊", label: "Analytics", active: false },
    { icon: "⚙️", label: "Settings", active: false },
    { icon: "🚪", label: "Logout", active: false },
  ];

  return (
    <aside
      className={`
        fixed inset-y-0 left-0 z-30
        flex flex-col
        transition-all duration-300 ease-in-out
        ${isCollapsed ? "w-16" : "w-64"}
        ${
          isDark
            ? "bg-gray-900 text-gray-100 border-r border-gray-800"
            : "bg-white text-gray-800 border-r border-gray-200 shadow-sm"
        }
      `}
    >
      {/* Logo / Brand */}
      <div
        className={`
        h-16 flex items-center justify-center border-b
        ${isDark ? "border-gray-800" : "border-gray-200"}
        ${isCollapsed ? "px-0" : "px-6"}
      `}
      >
        {!isCollapsed ? (
          <span className="text-xl font-bold tracking-tight">Admin</span>
        ) : (
          <span className="text-2xl font-black">A</span>
        )}
      </div>

      {/* Navigation */}
      <nav className="flex-1 px-3 py-6 space-y-1 overflow-y-auto">
        {menuItems.map((item, index) => (
          <button
            key={index}
            className={`
              w-full flex items-center gap-3 px-3 py-3 rounded-lg
              transition-colors duration-150 text-left
              ${isCollapsed ? "justify-center px-2" : ""}
              ${
                item.active
                  ? isDark
                    ? "bg-gray-800 text-white font-medium"
                    : "bg-blue-50 text-blue-700 font-medium"
                  : isDark
                    ? "text-gray-400 hover:bg-gray-800 hover:text-gray-200"
                    : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
              }
            `}
          >
            <span className="text-xl w-6 text-center flex-shrink-0">
              {item.icon}
            </span>
            {!isCollapsed && (
              <span className="font-medium truncate">{item.label}</span>
            )}
          </button>
        ))}
      </nav>

      {/* User section */}
      <div
        className={`
        p-4 border-t
        ${isDark ? "border-gray-800" : "border-gray-200"}
      `}
      >
        <div
          className={`
          flex items-center gap-3
          ${isCollapsed ? "justify-center" : ""}
        `}
        >
          <div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-medium shrink-0">
            T
          </div>
          {!isCollapsed && (
            <div className="min-w-0">
              <p className="font-medium truncate">Tuân</p>
              <p className="text-sm opacity-70 truncate">Admin</p>
            </div>
          )}
        </div>
      </div>
    </aside>
  );
}

Demo sử dụng (để kiểm tra):

jsx
function AppDemo() {
  const [collapsed, setCollapsed] = React.useState(false);
  const [theme, setTheme] = React.useState("light");

  return (
    <div
      className={`min-h-screen ${theme === "dark" ? "bg-gray-950" : "bg-gray-50"}`}
    >
      <div className="flex">
        <Sidebar isCollapsed={collapsed} theme={theme} />

        <div
          className={`flex-1 transition-all duration-300 ${collapsed ? "ml-16" : "ml-64"}`}
        >
          <header className="p-6 border-b bg-white dark:bg-gray-900 dark:border-gray-800">
            <div className="flex items-center justify-between">
              <h1 className="text-2xl font-bold">Dashboard</h1>
              <div className="flex gap-4">
                <button
                  onClick={() => setCollapsed(!collapsed)}
                  className="px-4 py-2 bg-gray-200 dark:bg-gray-800 rounded-lg"
                >
                  {collapsed ? "Expand" : "Collapse"}
                </button>
                <button
                  onClick={() => setTheme(theme === "light" ? "dark" : "light")}
                  className="px-4 py-2 bg-indigo-600 text-white rounded-lg"
                >
                  {theme === "light" ? "Dark" : "Light"} Mode
                </button>
              </div>
            </div>
          </header>
          <main className="p-8">
            <p>Nội dung chính ở đây...</p>
          </main>
        </div>
      </div>
    </div>
  );
}

Đặc điểm nổi bật:

  • Collapsible: width thay đổi mượt (transition-all), text ẩn khi collapsed
  • Active/Hover: background & text color thay đổi rõ ràng theo theme
  • Dark mode: chỉ cần thêm dark: prefix + class điều kiện
  • Responsive: sidebar fixed left, content tự động margin-left theo width
  • Clean code: className chia dòng, logic theme rõ ràng

→ Tailwind giúp implement nhanh, code dễ đọc và mở rộng (thêm dark mode, animation, responsive breakpoints chỉ cần thêm vài class).


⭐⭐⭐⭐⭐ Exercise 5: Production Challenge - Design System (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Build reusable design system foundation
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Tạo Design System với các components cơ bản:
 * 1. Button (5 variants, 3 sizes, loading state)
 * 2. Input (text, email, password, error state)
 * 3. Card (header, body, footer, variants)
 *
 * 🏗️ Technical Design Doc:
 *
 * 1. Component Architecture
 *    - Atomic design: Button, Input (atoms)
 *    - Card (molecule)
 *    - Shared theme system
 *
 * 2. Styling Strategy
 *    - CSS Modules cho component styles
 *    - CSS Variables cho theme
 *    - Inline styles cho dynamic props
 *
 * 3. API Design
 *    - Consistent prop names across components
 *    - Sensible defaults
 *    - TypeScript types (optional)
 *
 * 4. Performance Considerations
 *    - Hoist static styles
 *    - Avoid style recalculation
 *    - Minimal CSS specificity
 */

// Theme definition (CSS Variables)
/**
 * :root {
 *   --color-primary: #007bff;
 *   --color-secondary: #6c757d;
 *   --color-success: #28a745;
 *   --color-danger: #dc3545;
 *   --color-warning: #ffc107;
 *
 *   --spacing-xs: 4px;
 *   --spacing-sm: 8px;
 *   --spacing-md: 16px;
 *   --spacing-lg: 24px;
 *
 *   --radius-sm: 4px;
 *   --radius-md: 8px;
 *   --radius-lg: 12px;
 * }
 */

// ✅ Production Checklist:
// Component Quality:
// - [ ] Prop validation (PropTypes hoặc TypeScript)
// - [ ] Default props
// - [ ] Error boundaries (where applicable)
// - [ ] Loading states
// - [ ] Disabled states
// - [ ] Focus states (a11y)

// Styling Quality:
// - [ ] Consistent naming (BEM hoặc camelCase)
// - [ ] No hardcoded colors (use CSS vars)
// - [ ] Responsive (mobile-first)
// - [ ] Dark mode support
// - [ ] Smooth transitions

// Code Quality:
// - [ ] Clean, readable code
// - [ ] Comments for complex logic
// - [ ] No magic numbers
// - [ ] Reusable utilities

// 1️⃣ Button Component
/**
 * API:
 * <Button
 *   variant="primary|secondary|success|danger|warning"
 *   size="sm|md|lg"
 *   loading={boolean}
 *   disabled={boolean}
 *   onClick={function}
 * >
 *   Text
 * </Button>
 */

function Button({
  children,
  variant = "primary",
  size = "md",
  loading = false,
  disabled = false,
  onClick,
}) {
  // TODO: Implement
  // Requirements:
  // - Style dựa trên variant (colors from CSS vars)
  // - Size affects padding, font-size
  // - Loading state: show spinner, disable clicks
  // - Disabled state: reduced opacity, no pointer
  // - Hover/focus states
}

// 2️⃣ Input Component
/**
 * API:
 * <Input
 *   type="text|email|password"
 *   label="Label text"
 *   error="Error message"
 *   disabled={boolean}
 *   value={string}
 *   onChange={function}
 * />
 */

function Input({
  type = "text",
  label,
  error,
  disabled = false,
  value,
  onChange,
  ...props
}) {
  // TODO: Implement
  // Requirements:
  // - Label above input
  // - Error state (red border, error message below)
  // - Disabled state
  // - Focus state (blue border)
  // - Proper HTML structure (label + input)
}

// 3️⃣ Card Component
/**
 * API:
 * <Card variant="default|outlined|elevated">
 *   <Card.Header>Title</Card.Header>
 *   <Card.Body>Content</Card.Body>
 *   <Card.Footer>Actions</Card.Footer>
 * </Card>
 */

function Card({ children, variant = "default" }) {
  // TODO: Implement
  // Compound component pattern
}

Card.Header = function CardHeader({ children }) {
  // TODO: Implement
};

Card.Body = function CardBody({ children }) {
  // TODO: Implement
};

Card.Footer = function CardFooter({ children }) {
  // TODO: Implement
};

// 📝 Documentation Requirements:
/**
 * Tạo README.md với:
 * 1. Component overview
 * 2. Installation & setup
 * 3. Usage examples (code snippets)
 * 4. Props API table
 * 5. Customization guide (CSS vars)
 * 6. Accessibility notes
 */

// 🧪 Demo Page
function DesignSystemDemo() {
  return (
    <div style={{ padding: "40px", maxWidth: "800px", margin: "0 auto" }}>
      <h1>Design System Demo</h1>

      {/* Button Demo */}
      <section>
        <h2>Buttons</h2>
        <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
          <Button variant="primary">Primary</Button>
          <Button variant="secondary">Secondary</Button>
          <Button variant="success">Success</Button>
          <Button variant="danger">Danger</Button>
          <Button variant="warning">Warning</Button>
        </div>

        <div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
          <Button size="sm">Small</Button>
          <Button size="md">Medium</Button>
          <Button size="lg">Large</Button>
        </div>

        <div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
          <Button loading>Loading</Button>
          <Button disabled>Disabled</Button>
        </div>
      </section>

      {/* Input Demo */}
      <section style={{ marginTop: "40px" }}>
        <h2>Inputs</h2>
        <div style={{ maxWidth: "400px" }}>
          <Input label="Name" />
          <Input label="Email" type="email" />
          <Input label="Password" type="password" />
          <Input label="Error Example" error="This field is required" />
          <Input label="Disabled" disabled value="Cannot edit" />
        </div>
      </section>

      {/* Card Demo */}
      <section style={{ marginTop: "40px" }}>
        <h2>Cards</h2>
        <div
          style={{
            display: "grid",
            gap: "16px",
            gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
          }}
        >
          <Card variant="default">
            <Card.Header>Default Card</Card.Header>
            <Card.Body>Card content goes here</Card.Body>
            <Card.Footer>
              <Button size="sm">Action</Button>
            </Card.Footer>
          </Card>

          <Card variant="outlined">
            <Card.Header>Outlined Card</Card.Header>
            <Card.Body>With border only</Card.Body>
          </Card>

          <Card variant="elevated">
            <Card.Header>Elevated Card</Card.Header>
            <Card.Body>With shadow</Card.Body>
          </Card>
        </div>
      </section>
    </div>
  );
}
💡 Solution
jsx
// ================================================
// Thiết lập CSS Variables (thêm vào global CSS hoặc :root)
// ================================================
/*
:root {
  --color-primary: #0d6efd;
  --color-primary-hover: #0b5ed7;
  --color-secondary: #6c757d;
  --color-secondary-hover: #5a6268;
  --color-success: #198754;
  --color-success-hover: #157347;
  --color-danger: #dc3545;
  --color-danger-hover: #bb2d3b;
  --color-warning: #ffc107;
  --color-warning-hover: #e0a800;

  --color-text: #212529;
  --color-text-muted: #6c757d;
  --color-bg: #ffffff;
  --color-bg-muted: #f8f9fa;
  --color-border: #dee2e6;

  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;

  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
}
*/

// ================================================
// 1. Button Component
// ================================================
import buttonStyles from "./Button.module.css"; // giả sử bạn tạo file này

function Button({
  children,
  variant = "primary",
  size = "md",
  loading = false,
  disabled = false,
  onClick,
  ...props
}) {
  const baseClasses = buttonStyles.button;
  const sizeClass = buttonStyles[`size-${size}`];
  const variantClass = buttonStyles[variant];
  const stateClasses = [
    loading && buttonStyles.loading,
    disabled && buttonStyles.disabled,
  ]
    .filter(Boolean)
    .join(" ");

  const className = [baseClasses, sizeClass, variantClass, stateClasses]
    .filter(Boolean)
    .join(" ");

  return (
    <button
      className={className}
      disabled={disabled || loading}
      onClick={onClick}
      {...props}
    >
      {loading && <span className={buttonStyles.spinner} aria-hidden="true" />}
      <span style={{ visibility: loading ? "hidden" : "visible" }}>
        {children}
      </span>
    </button>
  );
}

// Button.defaultProps = {
//   variant: 'primary',
//   size: 'md',
//   loading: false,
//   disabled: false,
// };

// ================================================
// 2. Input Component
// ================================================
import inputStyles from "./Input.module.css";

function Input({
  type = "text",
  label,
  error,
  disabled = false,
  value,
  onChange,
  id = `input-${Math.random().toString(36).substr(2, 9)}`,
  ...props
}) {
  const wrapperClass = [
    inputStyles.wrapper,
    error && inputStyles.error,
    disabled && inputStyles.disabled,
  ]
    .filter(Boolean)
    .join(" ");

  return (
    <div className={wrapperClass}>
      {label && (
        <label htmlFor={id} className={inputStyles.label}>
          {label}
        </label>
      )}

      <input
        id={id}
        type={type}
        className={inputStyles.input}
        disabled={disabled}
        value={value}
        onChange={onChange}
        {...props}
      />

      {error && (
        <p className={inputStyles.errorMessage} role="alert">
          {error}
        </p>
      )}
    </div>
  );
}

// ================================================
// 3. Card Compound Component
// ================================================
import cardStyles from "./Card.module.css";

function Card({ children, variant = "default", className = "", ...props }) {
  const cardClass = [cardStyles.card, cardStyles[variant], className]
    .filter(Boolean)
    .join(" ");

  return (
    <div className={cardClass} {...props}>
      {children}
    </div>
  );
}

Card.Header = function CardHeader({ children, className = "" }) {
  return (
    <div className={[cardStyles.header, className].filter(Boolean).join(" ")}>
      {children}
    </div>
  );
};

Card.Body = function CardBody({ children, className = "" }) {
  return (
    <div className={[cardStyles.body, className].filter(Boolean).join(" ")}>
      {children}
    </div>
  );
};

Card.Footer = function CardFooter({ children, className = "" }) {
  return (
    <div className={[cardStyles.footer, className].filter(Boolean).join(" ")}>
      {children}
    </div>
  );
};

// ================================================
// Demo Page (giữ nguyên cấu trúc của bạn)
// ================================================
function DesignSystemDemo() {
  return (
    <div style={{ padding: "40px", maxWidth: "800px", margin: "0 auto" }}>
      <h1>Design System Demo</h1>

      {/* Button Demo */}
      <section>
        <h2>Buttons</h2>
        <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
          <Button variant="primary">Primary</Button>
          <Button variant="secondary">Secondary</Button>
          <Button variant="success">Success</Button>
          <Button variant="danger">Danger</Button>
          <Button variant="warning">Warning</Button>
        </div>

        <div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
          <Button size="sm">Small</Button>
          <Button size="md">Medium</Button>
          <Button size="lg">Large</Button>
        </div>

        <div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
          <Button loading>Loading</Button>
          <Button disabled>Disabled</Button>
        </div>
      </section>

      {/* Input Demo */}
      <section style={{ marginTop: "40px" }}>
        <h2>Inputs</h2>
        <div style={{ maxWidth: "400px" }}>
          <Input label="Name" placeholder="Enter name..." />
          <Input label="Email" type="email" placeholder="name@example.com" />
          <Input label="Password" type="password" placeholder="••••••••" />
          <Input
            label="Error Example"
            error="This field is required"
            placeholder="Type something..."
          />
          <Input label="Disabled" disabled value="Cannot edit this" />
        </div>
      </section>

      {/* Card Demo */}
      <section style={{ marginTop: "40px" }}>
        <h2>Cards</h2>
        <div
          style={{
            display: "grid",
            gap: "16px",
            gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
          }}
        >
          <Card variant="default">
            <Card.Header>Default Card</Card.Header>
            <Card.Body>
              <p>Some quick example text to build on the card title.</p>
            </Card.Body>
            <Card.Footer>
              <Button size="sm" variant="primary">
                Action
              </Button>
            </Card.Footer>
          </Card>

          <Card variant="outlined">
            <Card.Header>Outlined Card</Card.Header>
            <Card.Body>
              <p>With a nice blue border and minimal shadow.</p>
            </Card.Body>
          </Card>

          <Card variant="elevated">
            <Card.Header>Elevated Card</Card.Header>
            <Card.Body>
              <p>Has stronger shadow for depth effect.</p>
            </Card.Body>
          </Card>
        </div>
      </section>
    </div>
  );
}
markdown
### README.md (nội dung gợi ý)

# Design System – Basic Components

## Components Overview

- **Button** — Nút đa dạng với 5 variants, 3 kích thước, loading & disabled state
- **Input** — Trường nhập liệu với label, error, disabled, focus state
- **Card** — Card compound (Header / Body / Footer) với 3 variants

## Installation & Setup

1. Đảm bảo CSS Variables được định nghĩa trong `:root` (thường trong `index.css` hoặc `global.css`)
2. Tạo các file CSS Modules tương ứng: `Button.module.css`, `Input.module.css`, `Card.module.css`

## Usage Examples

```jsx
<Button variant="success" size="lg" onClick={handleSave}>
  Lưu thay đổi
</Button>

<Input
  label="Email"
  type="email"
  error="Email không hợp lệ"
  placeholder="name@example.com"
/>

<Card variant="elevated">
  <Card.Header>Thông tin đơn hàng</Card.Header>
  <Card.Body>Chi tiết đơn hàng...</Card.Body>
  <Card.Footer>
    <Button variant="primary">Xác nhận</Button>
  </Card.Footer>
</Card>
```
## Props API Table

### Button

| Prop     | Type    | Default   | Description                                  |
| -------- | ------- | --------- | -------------------------------------------- |
| variant  | string  | 'primary' | primary, secondary, success, danger, warning |
| size     | string  | 'md'      | sm, md, lg                                   |
| loading  | boolean | false     | Hiển thị spinner & disable click             |
| disabled | boolean | false     | Vô hiệu hóa nút                              |

### Input

| Prop     | Type    | Default | Description                 |
| -------- | ------- | ------- | --------------------------- |
| type     | string  | 'text'  | text, email, password       |
| label    | string  | —       | Nhãn phía trên input        |
| error    | string  | —       | Thông báo lỗi (hiển thị đỏ) |
| disabled | boolean | false   | Vô hiệu hóa input           |

### Card

| Prop    | Type   | Default   | Description                 |
| ------- | ------ | --------- | --------------------------- |
| variant | string | 'default' | default, outlined, elevated |

## Customization Guide

- Thay đổi màu sắc → chỉnh sửa CSS Variables trong `:root`
- Thay đổi khoảng cách → chỉnh `--spacing-*`
- Thay đổi bo góc → chỉnh `--radius-*`

## Accessibility Notes

- Button có `disabled` → screen reader đọc được
- Input có `label` + `htmlFor` → liên kết đúng
- Error message dùng `role="alert"`
- Focus state rõ ràng (outline + shadow)


**Ghi chú cuối:**

- Tất cả component sử dụng **CSS Modules** → scoped, an toàn khi scale
- Dùng **CSS Variables** → dễ theme (light/dark chỉ cần override root)
- Inline styles gần như không dùng (chỉ dùng cho dynamic nếu cần)
- Code clean, comment đủ, không magic number
- Đáp ứng đầy đủ checklist production (loading, disabled, focus, a11y cơ bản)

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh Trade-offs

FeatureInline StylesCSS ClassesCSS ModulesTailwind
Dynamic values⭐⭐⭐⭐⭐ Best⭐⭐ Via variables⭐⭐ Via variables⭐⭐⭐ Conditional
Pseudo-classes (:hover)❌ No✅ Yes✅ Yes✅ Yes
Media queries❌ No✅ Yes✅ Yes✅ Yes (md:, lg:)
Type safety✅ TypeScript❌ No⭐⭐⭐ Scoped⭐⭐ Class names
Bundle size⭐⭐⭐⭐ JS only⭐⭐⭐ Separate CSS⭐⭐⭐ Separate CSS⭐⭐⭐⭐ Tree-shaken
Developer experience⭐⭐ Verbose⭐⭐⭐⭐ Familiar⭐⭐⭐⭐ Clean⭐⭐⭐⭐⭐ Fast
Naming conflicts✅ None❌ Global scope✅ Scoped✅ Utility-based
Learning curve⭐⭐⭐⭐⭐ Easy⭐⭐⭐⭐⭐ Easy⭐⭐⭐⭐ Easy⭐⭐⭐ Moderate
Maintenance⭐⭐ Scattered⭐⭐⭐ Separate files⭐⭐⭐⭐ Colocated⭐⭐⭐⭐⭐ Inline

Decision Tree


Bạn đang style component, cần gì?

1. Style thay đổi dựa trên props/state (màu, size, position)?
   ├─ Chỉ vài properties → Inline Styles
   └─ Nhiều properties → Tailwind với conditional classes

2. Cần CSS features (hover, animations, media queries)?
   ├─ Simple project, ít components → CSS Classes
   ├─ Medium project, tránh conflicts → CSS Modules
   └─ Rapid development, design system → Tailwind

3. Team preference?
   ├─ Traditional CSS background → CSS Modules
   ├─ Utility-first mindset → Tailwind
   └─ Minimal setup → Inline + CSS Classes

4. Performance critical?
   ├─ Need tree-shaking → Tailwind
   ├─ Minimal CSS → Inline Styles
   └─ Standard → Any approach OK

5. Kích thước dự án?
   ├─ Small (<10 components) → CSS Classes OK
   ├─ Medium (10-50) → CSS Modules
   └─ Large (50+) → Tailwind hoặc CSS Modules

When to Use What - Detailed Guide

✅ Dùng Inline Styles khi:

jsx
// 1. Dynamic values từ props/state
function ProgressBar({ percent }) {
  return (
    <div style={{ width: "100%", background: "#eee" }}>
      <div
        style={{
          width: `${percent}%`, // Dynamic!
          height: "20px",
          background: "#007bff",
          transition: "width 0.3s ease",
        }}
      />
    </div>
  );
}

// 2. Animation values
function Draggable({ x, y }) {
  return (
    <div
      style={{
        position: "absolute",
        left: `${x}px`, // Real-time values
        top: `${y}px`,
        cursor: "grab",
      }}
    >
      Drag me
    </div>
  );
}

// 3. Theme colors từ context
function ThemedButton({ theme }) {
  return (
    <button
      style={{
        background: theme.primaryColor, // From theme object
        color: theme.textColor,
      }}
    >
      Click
    </button>
  );
}

✅ Dùng CSS Classes khi:

jsx
// 1. Simple apps không lo conflicts
// App.css
.header { /* ... */ }
.footer { /* ... */ }

// App.jsx
function App() {
  return (
    <>
      <header className="header">Logo</header>
      <main>Content</main>
      <footer className="footer">© 2024</footer>
    </>
  );
}

// 2. Global styles (resets, utilities)
// global.css
* { box-sizing: border-box; }
.container { max-width: 1200px; margin: 0 auto; }
.text-center { text-align: center; }

✅ Dùng CSS Modules khi:

jsx
// 1. Component library (tránh conflicts)
// Button.module.css → Button_primary__x1y2z
// Card.module.css → Card_primary__a3b4c (no conflict!)

// 2. Team lớn, nhiều người cùng code
// Mỗi dev code component riêng với CSS riêng → no conflicts

// 3. Cần CSS features + scoping
import styles from "./Modal.module.css";

function Modal() {
  return (
    <div className={styles.overlay}>
      <div className={styles.modal}>
        {/* :hover, :focus, animations đều work */}
      </div>
    </div>
  );
}

✅ Dùng Tailwind khi:

jsx
// 1. Rapid prototyping
function Prototype() {
  return (
    <div className="flex items-center gap-4 p-4 bg-white rounded-lg shadow">
      <img src="..." className="w-16 h-16 rounded-full" />
      <div>
        <h3 className="text-lg font-semibold">Name</h3>
        <p className="text-gray-600">Description</p>
      </div>
    </div>
  );
}

// 2. Consistent design system
// Tailwind enforces spacing scale (4px, 8px, 16px...)
// No random "padding: 13px" 😅

// 3. Responsive design
function ResponsiveCard() {
  return (
    <div
      className="
      w-full           // Mobile: full width
      md:w-1/2         // Tablet: 50%
      lg:w-1/3         // Desktop: 33%
      p-4              // All: padding 16px
      md:p-6           // Tablet+: padding 24px
    "
    >
      Card
    </div>
  );
}

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Inline Styles Object Recreation

jsx
// 🐛 BUG: Performance issue
function UserAvatar({ size = 48 }) {
  return (
    <img
      src="/avatar.jpg"
      style={{
        width: size,
        height: size,
        borderRadius: "50%",
      }} // ❌ New object every render!
    />
  );
}

Câu hỏi: Tại sao đây là performance issue?

💡 Giải thích

Vấn đề:

  • Style object { width: size, height: size, borderRadius: '50%' } được tạo mới mỗi render
  • React so sánh object bằng reference, không phải value
  • Mỗi render → object mới → React nghĩ style changed → re-apply styles

Fix 1: useMemo (sẽ học Ngày 33)

jsx
import { useMemo } from "react";

function UserAvatar({ size = 48 }) {
  const style = useMemo(
    () => ({
      width: size,
      height: size,
      borderRadius: "50%",
    }),
    [size],
  ); // Only recreate khi size thay đổi

  return <img src="/avatar.jpg" style={style} />;
}

Fix 2: Hoist static parts (current knowledge)

jsx
// ✅ Static style ở ngoài component
const baseStyle = { borderRadius: "50%" };

function UserAvatar({ size = 48 }) {
  return (
    <img
      src="/avatar.jpg"
      style={{
        ...baseStyle, // Reuse object
        width: size, // Only dynamic parts inline
        height: size,
      }}
    />
  );
}

📚 Vercel Best Practice: Hoist Static JSX

jsx
// ❌ Recreates element every render
function Container({ loading }) {
  return <div>{loading && <Spinner />}</div>;
}

// ✅ Reuses same element
const spinner = <Spinner />;
function Container({ loading }) {
  return <div>{loading && spinner}</div>;
}

Bug 2: CSS Modules Dynamic Class Names

jsx
// 🐛 BUG: Undefined class
import styles from "./Alert.module.css";

function Alert({ type }) {
  // type = "success" | "error" | "warning"
  return (
    <div className={styles.alert - { type }}>
      {" "}
      {/* ❌ Syntax error! */}
      Alert
    </div>
  );
}

Câu hỏi: Tại sao code này không chạy? Làm sao fix?

💡 Giải thích

Vấn đề:

  • Template literals KHÔNG work với object property access
  • styles.alert-${type} là invalid JavaScript syntax

Fix 1: Bracket notation

jsx
function Alert({ type }) {
  // ✅ Đúng syntax
  const alertClass = styles[`alert-${type}`];
  return <div className={alertClass}>Alert</div>;
}

Fix 2: CSS class mapping (cleaner)

css
/* Alert.module.css */
.alert {
  /* base */
}
.success {
  background: green;
}
.error {
  background: red;
}
.warning {
  background: yellow;
}
jsx
function Alert({ type }) {
  // ✅ Combine base + variant
  return <div className={`${styles.alert} ${styles[type]}`}>Alert</div>;
}

Fix 3: Map object (type-safe)

jsx
function Alert({ type }) {
  const typeStyles = {
    success: styles.success,
    error: styles.error,
    warning: styles.warning,
  };

  return <div className={`${styles.alert} ${typeStyles[type]}`}>Alert</div>;
}

Bug 3: Tailwind Conditional Classes

jsx
// 🐛 BUG: Classes không apply
function Badge({ count }) {
  return <span className={`badge ${count && "badge-active"}`}>{count}</span>;
}

// <Badge count={0} /> → Renders "0" trong className!
// → className="badge 0"

Câu hỏi: Tại sao count=0 lại xuất hiện trong className?

💡 Giải thích

Vấn đề:

  • count && 'badge-active' với count=0 → return 0 (falsy nhưng vẫn là value)
  • Template literal convert 0 thành string → "badge 0"

Fix: Explicit boolean

jsx
// ✅ Cách 1: Ternary
<span className={`badge ${count > 0 ? 'badge-active' : ''}`}>

// ✅ Cách 2: Double boolean
<span className={`badge ${!!count && 'badge-active'}`}>

// ✅ Cách 3: Separate logic (cleanest)
const badgeClass = ['badge', count > 0 && 'badge-active']
  .filter(Boolean)
  .join(' ');

<span className={badgeClass}>

📚 Vercel Best Practice:

jsx
// ❌ Dangerous với numbers
{
  count && <Badge />;
} // Renders "0" when count=0

// ✅ Explicit boolean
{
  count > 0 && <Badge />;
}
{
  count > 0 ? <Badge /> : null;
}

✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

markdown
- [ ] Tôi hiểu 4 cách styling trong React
- [ ] Tôi biết khi nào dùng Inline Styles (dynamic values)
- [ ] Tôi biết khi nào dùng CSS Modules (scoping)
- [ ] Tôi biết khi nào dùng Tailwind (rapid development)
- [ ] Tôi hiểu trade-offs giữa các approaches
- [ ] Tôi biết cách conditional styling với className
- [ ] Tôi biết CSS Modules tránh conflicts như thế nào
- [ ] Tôi biết Tailwind chỉ dùng core classes trong artifacts

Code Review Checklist

markdown
#### Inline Styles:

- [ ] Static styles được hoist ra ngoài component?
- [ ] Object không bị recreate mỗi render?
- [ ] Chỉ dùng cho dynamic values?

#### CSS Classes:

- [ ] className dùng template literals cho conditional?
- [ ] Tránh dùng && với numbers (0, NaN)?
- [ ] Classes được organize rõ ràng?

#### CSS Modules:

- [ ] File naming convention: `Component.module.css`?
- [ ] Import: `import styles from './Component.module.css'`?
- [ ] Dynamic classes dùng bracket notation?

#### Tailwind:

- [ ] Chỉ dùng core utility classes?
- [ ] Classes được organize (line breaks)?
- [ ] Responsive prefixes (md:, lg:) đúng?
- [ ] Conditional classes với ternary?

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

Bài 1: Style Product List với 3 cách khác nhau

Implement cùng một UI với 3 approaches:

  1. Version 1: CSS Classes
  2. Version 2: CSS Modules
  3. Version 3: Tailwind

UI Spec:

┌──────────────────────────────────────┐
│ Products                [Grid View]  │
├──────────────────────────────────────┤
│ ┌──────┐  ┌──────┐  ┌──────┐         │
│ │ Img  │  │ Img  │  │ Img  │         │
│ │ Name │  │ Name │  │ Name │         │
│ │ Price│  │ Price│  │ Price│         │
│ │ [Buy]│  │ [Buy]│  │ [Buy]│         │
│ └──────┘  └──────┘  └──────┘         │
└──────────────────────────────────────┘

Requirements:

  • Grid layout (3 columns)
  • Card hover effect (shadow)
  • Button hover (darker)
  • Responsive: mobile = 1 column

Compare: Viết nhận xét về DX, code length, maintainability của từng cách.

💡 Solution
jsx
// Dữ liệu mẫu dùng chung cho cả 3 version
const products = [
  { id: 1, name: "iPhone 16 Pro", price: 34990000, image: "https://picsum.photos/seed/iphone/300/300" },
  { id: 2, name: "MacBook Pro M4", price: 49990000, image: "https://picsum.photos/seed/macbook/300/300" },
  { id: 3, name: "AirPods Max", price: 14990000, image: "https://picsum.photos/seed/airpods/300/300" },
  { id: 4, name: "Apple Watch Ultra 2", price: 21990000, image: "https://picsum.photos/seed/watch/300/300" },
];

// ────────────────────────────────────────────────
// VERSION 1: CSS Classes (global CSS)
// ────────────────────────────────────────────────
/* File: ProductGrid.css (global) */
.product-grid-container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 32px 16px;
}

.product-grid-title {
  font-size: 1.75rem;
  font-weight: 700;
  margin-bottom: 24px;
  text-align: center;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}

@media (max-width: 1024px) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 640px) {
  .product-grid {
    grid-template-columns: 1fr;
  }
}

.product-card {
  background: white;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid #e5e7eb;
  transition: all 0.25s ease;
}

.product-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 10px 20px rgba(0,0,0,0.12);
}

.product-image {
  width: 100%;
  height: 240px;
  object-fit: cover;
}

.product-info {
  padding: 16px;
}

.product-name {
  font-size: 1.125rem;
  font-weight: 600;
  margin: 0 0 8px;
  line-height: 1.4;
}

.product-price {
  font-size: 1.25rem;
  font-weight: 700;
  color: #2563eb;
  margin-bottom: 12px;
}

.buy-button {
  width: 100%;
  padding: 10px;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.buy-button:hover {
  background: #1d4ed8;
}

function ProductGridCSSClasses() {
  return (
    <div className="product-grid-container">
      <h2 className="product-grid-title">Products (CSS Classes)</h2>
      <div className="product-grid">
        {products.map(product => (
          <div key={product.id} className="product-card">
            <img src={product.image} alt={product.name} className="product-image" />
            <div className="product-info">
              <h3 className="product-name">{product.name}</h3>
              <p className="product-price">{product.price.toLocaleString('vi-VN')}₫</p>
              <button className="buy-button">Mua ngay</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────
// VERSION 2: CSS Modules
// ────────────────────────────────────────────────
/* File: ProductGrid.module.css */
.gridContainer {
  max-width: 1400px;
  margin: 0 auto;
  padding: 32px 16px;
}

.title {
  font-size: 1.75rem;
  font-weight: 700;
  margin-bottom: 24px;
  text-align: center;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}

@media (max-width: 1024px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 640px) {
  .grid {
    grid-template-columns: 1fr;
  }
}

.card {
  background: white;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid #e5e7eb;
  transition: all 0.25s ease;
}

.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 10px 20px rgba(0,0,0,0.12);
}

.image {
  width: 100%;
  height: 240px;
  object-fit: cover;
}

.info {
  padding: 16px;
}

.name {
  font-size: 1.125rem;
  font-weight: 600;
  margin: 0 0 8px;
  line-height: 1.4;
}

.price {
  font-size: 1.25rem;
  font-weight: 700;
  color: #2563eb;
  margin-bottom: 12px;
}

.button {
  width: 100%;
  padding: 10px;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.button:hover {
  background: #1d4ed8;
}

import styles from './ProductGrid.module.css';

function ProductGridCSSModules() {
  return (
    <div className={styles.gridContainer}>
      <h2 className={styles.title}>Products (CSS Modules)</h2>
      <div className={styles.grid}>
        {products.map(product => (
          <div key={product.id} className={styles.card}>
            <img src={product.image} alt={product.name} className={styles.image} />
            <div className={styles.info}>
              <h3 className={styles.name}>{product.name}</h3>
              <p className={styles.price}>{product.price.toLocaleString('vi-VN')}₫</p>
              <button className={styles.button}>Mua ngay</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────
// VERSION 3: Tailwind CSS
// ────────────────────────────────────────────────
function ProductGridTailwind() {
  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
      <h2 className="text-3xl font-bold text-gray-900 mb-10 text-center">
        Products (Tailwind)
      </h2>

      <div className="
        grid grid-cols-1
        sm:grid-cols-2
        lg:grid-cols-3
        gap-6 md:gap-8
      ">
        {products.map(product => (
          <div
            key={product.id}
            className="
              bg-white rounded-xl overflow-hidden border border-gray-200
              transition-all duration-300
              hover:shadow-2xl hover:-translate-y-2 hover:border-gray-300
            "
          >
            <img
              src={product.image}
              alt={product.name}
              className="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-105"
            />
            <div className="p-5">
              <h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">
                {product.name}
              </h3>
              <p className="text-xl font-bold text-blue-600 mb-4">
                {product.price.toLocaleString('vi-VN')}₫
              </p>
              <button className="
                w-full py-3 px-4 bg-blue-600 text-white font-semibold
                rounded-lg hover:bg-blue-700 active:bg-blue-800
                transition-colors duration-200
              ">
                Mua ngay
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

So sánh 3 cách tiếp cận (DX, code length, maintainability)

Tiêu chíVersion 1: CSS Classes (global)Version 2: CSS ModulesVersion 3: Tailwind CSS
Code length (JSX)Ngắn, dễ đọcNgắn, rất sạch (className ngắn)Dài nhất (nhiều class), nhưng cấu trúc rõ ràng
Code length (CSS)Dài, phải đặt tên class thủ côngDài, nhưng scoped → an toànKhông cần file CSS → ngắn nhất về tổng thể
Developer Experience (DX)Quen thuộc với dev truyền thốngTốt nhất về scoping & tổ chứcNhanh nhất khi viết, không cần chuyển tab
MaintainabilityDễ xung đột khi dự án lớnTốt nhất (scoped, colocation)Rất tốt (utility classes dễ tìm, nhất quán)
ResponsiveCần viết media query thủ côngCần viết media query thủ côngSiêu tiện (sm:, md:, lg:,...)
Dynamic stylingDễ kết hợp template literalsDễ với bracket notationRất tốt với template literals + điều kiện
Khi nào dùngDự án rất nhỏ, team nhỏ, không lo conflictDự án trung bình/lớn, component libraryDự án cần tốc độ, design system, prototype nhanh
Điểm tổng (cho dashboard admin)6/108.5/109/10 (lựa chọn tốt nhất hiện nay)

Kết luận ngắn gọn:

  • CSS Classes → phù hợp học tập, dự án rất nhỏ
  • CSS Modules → lựa chọn an toàn nhất cho dự án trung bình/lớn (scoped, dễ maintain lâu dài)
  • Tailwind → nhanh nhất về phát triển, dễ mở rộng responsive/dark mode, được cộng đồng hiện đại ưa chuộng nhất cho dashboard & SaaS admin panel năm 2025–2026

Nếu là tech lead startup, Tailwind thường là lựa chọn thắng thế trong hầu hết các trường hợp hiện nay (đặc biệt khi team < 10 người và cần iterate nhanh).


Nâng cao (60 phút)

Bài 2: Theme Switcher Component

Build component toggle giữa Light/Dark theme:

jsx
/**
 * Requirements:
 * 1. Button toggle theme
 * 2. Apply theme cho toàn bộ app
 * 3. Persist theme (localStorage - đã học Ngày 7)
 * 4. Smooth transition giữa themes
 *
 * Technical:
 * - Dùng CSS variables cho colors
 * - Inline styles cho dynamic theme
 * - localStorage cho persistence
 */

Gợi ý structure:

jsx
// Theme object
const themes = {
  light: {
    background: "#ffffff",
    text: "#000000",
    primary: "#007bff",
  },
  dark: {
    background: "#1a1a1a",
    text: "#ffffff",
    primary: "#0dcaf0",
  },
};

// Component
function App() {
  const [theme, setTheme] = useState("light");

  // TODO:
  // 1. Load theme từ localStorage
  // 2. Apply theme styles
  // 3. Save khi theme changes

  return (
    <div
      style={{
        background: themes[theme].background,
        color: themes[theme].text,
      }}
    >
      {/* Your UI */}
    </div>
  );
}
💡 Solution
jsx
/**
 * ThemeSwitcher - Component toggle Light/Dark theme với persistence & transition mượt
 *
 * Features:
 * - Nút toggle theme (☀️ / 🌙)
 * - Áp dụng theme toàn app qua CSS variables
 * - Lưu theme vào localStorage (persist khi refresh)
 * - Transition mượt giữa các theme
 * - Responsive & clean UI demo
 */
function ThemeSwitcher() {
  // Khởi tạo theme từ localStorage (hoặc mặc định 'light')
  const [theme, setTheme] = React.useState(() => {
    const saved = localStorage.getItem("theme");
    return saved || "light";
  });

  // Áp dụng theme lên :root và lưu vào localStorage khi theme thay đổi
  React.useEffect(() => {
    // Lưu vào localStorage
    localStorage.setItem("theme", theme);

    // Cập nhật data-theme attribute để dễ override CSS nếu cần
    document.documentElement.setAttribute("data-theme", theme);

    // Optional: thêm class 'dark' cho tailwind dark mode (nếu dùng tailwind)
    if (theme === "dark") {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  }, [theme]);

  // Toggle function
  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  // Theme definitions (dùng CSS variables)
  // Bạn có thể mở rộng thêm nhiều biến hơn
  const themeStyles = {
    light: {
      "--bg-primary": "#ffffff",
      "--bg-secondary": "#f8f9fa",
      "--text-primary": "#212529",
      "--text-secondary": "#495057",
      "--primary": "#0d6efd",
      "--primary-hover": "#0b5ed7",
      "--border": "#dee2e6",
      "--shadow": "0 4px 6px -1px rgba(0,0,0,0.1)",
    },
    dark: {
      "--bg-primary": "#121212",
      "--bg-secondary": "#1e1e1e",
      "--text-primary": "#e0e0e0",
      "--text-secondary": "#9e9e9e",
      "--primary": "#0dcaf0",
      "--primary-hover": "#0aa2c0",
      "--border": "#333333",
      "--shadow": "0 4px 6px -1px rgba(0,0,0,0.5)",
    },
  };

  // Áp dụng CSS variables động (chạy mỗi khi theme thay đổi)
  React.useEffect(() => {
    const root = document.documentElement;
    Object.entries(themeStyles[theme]).forEach(([key, value]) => {
      root.style.setProperty(key, value);
    });
  }, [theme]);

  // Thêm transition mượt cho toàn bộ trang
  React.useEffect(() => {
    document.documentElement.style.transition =
      "background-color 0.4s ease, color 0.4s ease";
  }, []);

  return (
    <div
      className="min-h-screen transition-colors duration-400"
      style={{
        backgroundColor: "var(--bg-primary)",
        color: "var(--text-primary)",
      }}
    >
      {/* Header */}
      <header
        className="sticky top-0 z-10 border-b shadow-sm"
        style={{
          backgroundColor: "var(--bg-secondary)",
          borderColor: "var(--border)",
        }}
      >
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
          <h1 className="text-2xl font-bold">Theme Switcher Demo</h1>

          <button
            onClick={toggleTheme}
            className="
              px-5 py-2.5 rounded-full font-medium
              transition-all duration-300 transform hover:scale-105
              flex items-center gap-2
            "
            style={{
              backgroundColor: "var(--primary)",
              color: "white",
            }}
            onMouseEnter={(e) => {
              e.currentTarget.style.backgroundColor = "var(--primary-hover)";
            }}
            onMouseLeave={(e) => {
              e.currentTarget.style.backgroundColor = "var(--primary)";
            }}
          >
            {theme === "light" ? (
              <>🌙 Chuyển sang Dark Mode</>
            ) : (
              <>☀️ Chuyển sang Light Mode</>
            )}
          </button>
        </div>
      </header>

      {/* Main content demo */}
      <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          {Array.from({ length: 6 }).map((_, i) => (
            <div
              key={i}
              className="
                rounded-xl p-6 shadow-md
                transition-all duration-300 hover:shadow-xl hover:-translate-y-1
              "
              style={{
                backgroundColor: "var(--bg-secondary)",
                border: "1px solid var(--border)",
              }}
            >
              <h3 className="text-xl font-semibold mb-3">Card {i + 1}</h3>
              <p className="mb-4" style={{ color: "var(--text-secondary)" }}>
                Đây là nội dung mẫu để thể hiện theme. Màu nền, chữ, viền sẽ tự
                động thay đổi khi chuyển theme.
              </p>
              <button
                className="
                  px-4 py-2 rounded-lg font-medium text-white
                  transition-colors duration-200
                "
                style={{
                  backgroundColor: "var(--primary)",
                }}
              >
                Action
              </button>
            </div>
          ))}
        </div>
      </main>

      {/* Footer */}
      <footer
        className="border-t py-6 text-center text-sm"
        style={{
          borderColor: "var(--border)",
          color: "var(--text-secondary)",
        }}
      >
        <p>
          Theme được lưu trong localStorage • Ngày hiện tại:{" "}
          {new Date().toLocaleDateString("vi-VN")}
        </p>
      </footer>
    </div>
  );
}

Hướng dẫn sử dụng & kiểm tra:

  1. Copy component ThemeSwitcher vào file và render trong App
  2. Click nút toggle → theme thay đổi mượt (transition 0.4s)
  3. Refresh trang → theme vẫn giữ nguyên (do localStorage)
  4. Kiểm tra DevTools → xem :root có các --bg-primary, --text-primary, v.v. thay đổi đúng

Lưu ý kỹ thuật:

  • Sử dụng data-theme attribute + CSS variables → dễ mở rộng dark mode với Tailwind (nếu sau này dùng)
  • Transition áp dụng cho background-colorcolor → mượt mà nhất
  • Loading state không cần vì theme switch rất nhanh
  • Responsive: layout grid tự động điều chỉnh theo kích thước màn hình

Khi học thêm prefers-color-scheme, bạn có thể thêm:

jsx
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initialTheme = saved || (prefersDark ? "dark" : "light");

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Styling:

  2. CSS Modules Official:

  3. Tailwind CSS Docs (Core Utilities):

Đọc thêm

  1. Vercel Best Practices:

    • Conditional Rendering (avoiding 0, NaN)
    • SVG Performance (animate wrapper)
    • Hoisting static JSX
  2. CSS Performance:

  3. Accessibility:


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền (đã học):

  • Ngày 3: JSX syntax → className attribute
  • Ngày 4: Props → truyền variant, size
  • Ngày 5: Conditional rendering → conditional classes
  • Ngày 6: Lists → render multiple styled items
  • Ngày 7: Component composition → Card.Header pattern

Hướng tới (sẽ dùng):

  • Ngày 11: useState → dynamic theme switching
  • Ngày 16: useEffect → detect dark mode preference
  • Ngày 21: useRef → animate element measurements
  • Ngày 33: useMemo → optimize style calculations
  • Ngày 46: useTransition → smooth theme transitions
  • Module C (Ngày 91-95): Deep dive Styling (styled-components, Emotion)

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Bundle Size Impact:

jsx
// ❌ Import toàn bộ icon library
import { Icon } from "huge-icon-library"; // 500KB!

// ✅ Import chỉ icon cần (tree-shaking)
import CheckIcon from "huge-icon-library/icons/check"; // 2KB

// 📊 Impact: 250× nhỏ hơn

2. CSS Specificity Wars:

css
/* ❌ Specificity hell */
.card .header .title {
}
.card.featured .header .title {
} /* Must override */
.card.featured.large .header .title {
} /* More specific */

/* ✅ Flat structure (BEM hoặc CSS Modules) */
.card__title {
}
.card__title--featured {
}
.card__title--large {
}

3. Runtime vs Build-time Styles:

jsx
// Runtime: CSS-in-JS (styled-components)
// - Pros: Dynamic, theme-aware
// - Cons: JS bundle, runtime overhead

// Build-time: CSS Modules, Tailwind
// - Pros: Optimized, tree-shaken
// - Cons: Less dynamic

// 📊 Real numbers from Vercel blog:
// CSS-in-JS: +40KB JS, runtime parse
// Tailwind: -60% unused CSS with PurgeCSS

4. Dark Mode Strategy:

jsx
// ❌ Inline styles (hard to maintain)
<div style={{ background: isDark ? '#000' : '#fff' }}>

// ❌ Duplicate CSS
.light-theme .card { background: white; }
.dark-theme .card { background: black; }

// ✅ CSS Variables (best)
:root {
  --bg: white;
  --text: black;
}

[data-theme="dark"] {
  --bg: black;
  --text: white;
}

.card { background: var(--bg); color: var(--text); }

Câu Hỏi Phỏng Vấn

Junior Level:

Q1: "So sánh inline styles và CSS classes trong React?"

Expected Answer
  • Inline styles: JSX object, camelCase, good for dynamic values, no pseudo-classes
  • CSS classes: string className, kebab-case in CSS, full CSS power, potential conflicts
  • Trade-off: Dynamic → inline, Static → CSS

Q2: "CSS Modules giải quyết vấn đề gì?"

Expected Answer
  • Scoping: Class names được hash, tránh conflicts
  • Import như JS module: import styles from './file.module.css'
  • Vẫn dùng CSS features (hover, media queries)

Mid Level:

Q3: "Khi nào nên dùng CSS-in-JS thay vì CSS Modules?"

Expected Answer

CSS-in-JS (styled-components) khi:

  • Cần dynamic styles based on complex props
  • Theme switching frequency cao
  • Component library với many variants

CSS Modules khi:

  • Performance critical (no runtime)
  • Team familiar với traditional CSS
  • Static styles chủ yếu

Q4: "Optimize performance của inline styles thế nào?"

Expected Answer
  1. Hoist static parts bên ngoài component
  2. useMemo cho complex calculations (sẽ học Ngày 33)
  3. Avoid recreating object mỗi render
  4. Consider CSS Classes nếu không truly dynamic

Senior Level:

Q5: "Architecture decision: Tailwind vs CSS Modules cho app 100+ components?"

Expected Answer

Analysis:

Tailwind:

  • Pros: Consistent design tokens, fast iteration, tree-shakeable
  • Cons: Learning curve, HTML clutter, hard to enforce custom brand

CSS Modules:

  • Pros: Full CSS control, team familiar, clear separation
  • Cons: Naming overhead, potential duplication, larger CSS bundle

Decision Framework:

  • Has design system? → Tailwind (enforces constraints)
  • Custom/branded UI? → CSS Modules (full control)
  • Team size >10? → Tailwind (consistency)
  • Timeline tight? → Tailwind (faster)

Hybrid Approach:

  • Tailwind for layout/utilities
  • CSS Modules for complex components
  • CSS Variables for theming

War Stories

Story 1: The className="0" Bug

jsx
// Production bug tại Vercel Dashboard (real story)

// ❌ Original code
function Badge({ count }) {
  return (
    <span className={`badge ${count && 'has-count'}`}>
      {count}
    </span>
  );
}

// User có 0 notifications → HTML: <span className="badge 0">0</span>
// CSS selector .badge.0 failed → no styles applied!

// ✅ Fix
<span className={`badge ${count > 0 ? 'has-count' : ''}`}>

Lesson: Always explicit booleans với numbers


Story 2: CSS Modules Hash Collision

jsx
// Extremely rare, but happened:

// ComponentA.module.css → .button__a1b2
// ComponentB.module.css → .button__a1b2  (same hash!)

// Root cause: Identical content + webpack config
// Fix: Update webpack css-loader hashStrategy

Lesson: CSS Modules không 100% collision-free (though extremely rare)


Story 3: Tailwind Purge Removed Needed Classes

jsx
// Dynamic class names bị purge (removed from production build)

// ❌ This won't work
const colors = ['red', 'blue', 'green'];
<div className={`bg-${colors[index]}-500`}>  // Purged!

// ✅ Safelist hoặc use full names
<div className={
  index === 0 ? 'bg-red-500' :
  index === 1 ? 'bg-blue-500' :
  'bg-green-500'
}>

Lesson: Tailwind PurgeCSS scans code statically - no dynamic strings


🎓 TỔNG KẾT NGÀY 8

Bạn đã học được:

4 cách styling: Inline, CSS Classes, CSS Modules, Tailwind ✅ Decision tree: Khi nào dùng cách nào ✅ Trade-offs: Không có perfect solution ✅ Production patterns: Hoisting, CSS vars, conditional classes ✅ Vercel best practices: Conditional rendering, static hoisting, SVG optimization

Key Takeaways:

  1. Inline Styles: Dynamic values only, hoist static parts
  2. CSS Classes: Simple apps, watch out for conflicts
  3. CSS Modules: Scoped styles, full CSS power, best for medium+ apps
  4. Tailwind: Utility-first, rapid development, enforce design system

Ngày mai (Ngày 9):

Forms Controlled - Part 1 (KHÔNG STATE)

Chúng ta sẽ học:

  • Form elements trong React
  • Controlled vs Uncontrolled concept (lý thuyết)
  • Event.target.value
  • Form validation cơ bản

⚠️ Chú ý: Ngày 9 chỉ dạy CONCEPT của controlled forms, CHƯA implement (vì chưa học useState). Implementation thực tế sẽ là Ngày 13 (sau khi học useState patterns).


Personal tech knowledge base