Tailwind CSS

Responsive Design & Dark Mode

Tailwind CSS v4 — responsive breakpoints, container queries, dark mode, custom variants. Bài tập responsive layout và dark mode theme.

1. Responsive Design

Responsive design là kỹ năng bắt buộc với mọi frontend developer. Tailwind CSS v4 áp dụng triết lý mobile-first: bạn viết style cho mobile trước, sau đó dùng breakpoint prefix để override cho màn hình lớn hơn.

1.1. Breakpoint System

PrefixMin-widthThiết bị tiêu biểu
(không prefix)0pxMobile (mặc định)
sm:640pxMobile landscape, tablet nhỏ
md:768pxTablet dọc
lg:1024pxTablet ngang, laptop nhỏ
xl:1280pxLaptop, desktop
2xl:1536pxDesktop rộng, màn hình lớn
Mobile-first là gì? Bạn bắt đầu với style cho màn hình nhỏ nhất (mobile), sau đó dùng md:, lg: để override khi màn hình rộng ra. Điều này trái ngược với desktop-first (bắt đầu từ desktop rồi ẩn bớt trên mobile). Mobile-first giúp code ít hơn, tối ưu performance cho mobile devices.

Ví dụ cơ bản:

<!-- Mobile-first: 1 column trên mobile, 2 columns từ md trở lên, 3 columns từ lg -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <div class="bg-blue-100 p-4 rounded">Card 1</div>
  <div class="bg-blue-100 p-4 rounded">Card 2</div>
  <div class="bg-blue-100 p-4 rounded">Card 3</div>
  <div class="bg-blue-100 p-4 rounded">Card 4</div>
  <div class="bg-blue-100 p-4 rounded">Card 5</div>
  <div class="bg-blue-100 p-4 rounded">Card 6</div>
</div>

Giải thích:

  • grid-cols-1 (mobile): 1 cột
  • md:grid-cols-2 (tablet trở lên): 2 cột
  • lg:grid-cols-3 (laptop trở lên): 3 cột

1.2. Responsive Utilities — mọi thứ đều responsive được

Hầu như mọi utility class trong Tailwind đều có thể gắn breakpoint prefix. Bạn có thể thay đổi font size, padding, margin, display, flex direction, color, v.v... dựa trên kích thước màn hình.

<div class="
  text-sm md:text-base lg:text-lg
  px-4 md:px-8 lg:px-16
  flex flex-col md:flex-row
  hidden md:block
  w-full md:w-1/2 lg:w-1/3
  bg-gray-100 md:bg-white lg:bg-gray-50
">
  Responsive ở mọi khía cạnh
</div>
Câu PV:"Tại sao nói Tailwind khiến responsive design dễ hơn CSS truyền thống?"

"Vì với Tailwind, bạn không cần viết media query riêng. Mỗi class đều có thể responsive bằng cách thêm prefix. Điều này giữ code gần với element — bạn thấy ngay element đó sẽ trông như thế nào ở từng breakpoint, không cần scroll lên xuống giữa HTML và CSS file. Cộng với mobile-first mindset được built-in vào design system — spacing, font size scale được tính toán để hoạt động tốt trên mọi kích thước."

1.3. Customizing Breakpoints

Trong Tailwind CSS v4, bạn có thể tùy chỉnh breakpoints qua @theme:

@import "tailwindcss";

@theme {
  --breakpoint-xs: 30rem;   /* 480px */
  --breakpoint-sm: 40rem;   /* 640px */
  --breakpoint-md: 48rem;   /* 768px */
  --breakpoint-lg: 64rem;   /* 1024px */
  --breakpoint-xl: 80rem;   /* 1280px */
  --breakpoint-2xl: 96rem;  /* 1536px */
  --breakpoint-3xl: 120rem; /* 1920px — custom mới */
}
Bạn cũng có thể dùng max-width breakpoints bằng cách thêm max- prefix. Tuy nhiên, với mobile-first approach, bạn hiếm khi cần dùng. Max-width breakpoints hữu ích trong vài case đặc biệt: max-md:hidden (ẩn khi màn hình nhỏ hơn md).

1.4. Responsive Patterns Thường Gặp

<!-- Pattern 1: Stack trên mobile, side-by-side trên desktop -->
<div class="flex flex-col lg:flex-row gap-4">
  <aside class="lg:w-1/4 bg-gray-100 p-4">Sidebar</aside>
  <main class="lg:w-3/4 bg-white p-4">Main Content</main>
</div>

<!-- Pattern 2: Ẩn/hiện element dựa trên màn hình -->
<nav>
  <!-- Hamburger icon — chỉ hiện trên mobile -->
  <button class="block lg:hidden">
    <svg><!-- hamburger icon --></svg>
  </button>
  <!-- Full menu — chỉ hiện từ lg trở lên -->
  <ul class="hidden lg:flex gap-6">
    <li><a href="#">Home</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
</nav>

<!-- Pattern 3: Font size scale theo màn hình -->
<h1 class="text-2xl md:text-4xl lg:text-5xl font-bold">
  Hero Headline
</h1>

<!-- Pattern 4: Padding tăng dần theo màn hình -->
<section class="px-4 py-8 md:px-8 md:py-12 lg:px-16 lg:py-20">
  <p>Section với padding responsive</p>
</section>

1.5. BÀI TẬP 1 — Navigation: Hamburger Mobile → Full Menu Desktop

Yêu cầu: Xây dựng navigation bar responsive:

  • Mobile (< 768px): Logo bên trái, hamburger button bên phải. Click hamburger → menu dropdown hiện ra.
  • Tablet/Desktop (>= 768px): Logo + menu items nằm ngang + CTA button.
  • Fixed top, có backdrop-blur khi scroll.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi" class="scroll-smooth">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Navigation - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="min-h-screen bg-gray-50">

  <!-- Navigation -->
  <header class="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
    <nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
      <div class="flex items-center justify-between h-16">
        
        <!-- Logo -->
        <div class="flex-shrink-0">
          <a href="#" class="text-xl font-bold text-gray-900">
            Brand<span class="text-blue-600">Name</span>
          </a>
        </div>

        <!-- Desktop Menu (hidden on mobile) -->
        <div class="hidden md:flex md:items-center md:gap-8">
          <a href="#features" class="text-gray-600 hover:text-gray-900 transition-colors">Features</a>
          <a href="#pricing" class="text-gray-600 hover:text-gray-900 transition-colors">Pricing</a>
          <a href="#about" class="text-gray-600 hover:text-gray-900 transition-colors">About</a>
          <a href="#contact" class="text-gray-600 hover:text-gray-900 transition-colors">Contact</a>
          <a href="#" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm">
            Get Started
          </a>
        </div>

        <!-- Mobile Hamburger Button (hidden on desktop) -->
        <button id="menu-toggle" class="md:hidden p-2 rounded-lg text-gray-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" aria-label="Toggle menu">
          <!-- Hamburger icon -->
          <svg id="menu-icon-open" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
          </svg>
          <!-- Close icon (hidden by default) -->
          <svg id="menu-icon-close" class="w-6 h-6 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
          </svg>
        </button>
      </div>

      <!-- Mobile Menu Dropdown (hidden by default) -->
      <div id="mobile-menu" class="md:hidden hidden pb-4 border-t border-gray-100">
        <div class="flex flex-col gap-2 pt-4">
          <a href="#features" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors">Features</a>
          <a href="#pricing" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors">Pricing</a>
          <a href="#about" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors">About</a>
          <a href="#contact" class="block px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors">Contact</a>
          <a href="#" class="block px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-center">
            Get Started
          </a>
        </div>
      </div>
    </nav>
  </header>

  <!-- Spacer để nội dung không bị che bởi fixed header -->
  <div class="h-16"></div>

  <!-- Demo Content -->
  <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
    <section id="features" class="py-16">
      <h2 class="text-3xl font-bold mb-8">Features Section</h2>
      <p class="text-gray-600">Scroll để test fixed header với backdrop-blur.</p>
    </section>
    <section id="pricing" class="py-16">
      <h2 class="text-3xl font-bold mb-8">Pricing Section</h2>
    </section>
  </main>

  <script>
    const menuToggle = document.getElementById('menu-toggle');
    const mobileMenu = document.getElementById('mobile-menu');
    const iconOpen = document.getElementById('menu-icon-open');
    const iconClose = document.getElementById('menu-icon-close');

    menuToggle.addEventListener('click', () => {
      mobileMenu.classList.toggle('hidden');
      iconOpen.classList.toggle('hidden');
      iconClose.classList.toggle('hidden');
    });

    // Đóng menu khi click vào link
    mobileMenu.querySelectorAll('a').forEach(link => {
      link.addEventListener('click', () => {
        mobileMenu.classList.add('hidden');
        iconOpen.classList.remove('hidden');
        iconClose.classList.add('hidden');
      });
    });
  </script>

</body>
</html>

Giải thích key decisions:

  • fixed top-0 left-0 right-0 z-50: Header luôn nằm trên cùng, bất kể scroll.
  • bg-white/80 backdrop-blur-md: Hiệu ứng glassmorphism — nền trong suốt nhẹ với blur phía sau.
  • hidden md:flex: Desktop menu ẩn trên mobile, hiện trên tablet trở lên.
  • md:hidden: Hamburger button chỉ hiện trên mobile.
  • aria-label="Toggle menu": Accessibility — screen reader biết đây là nút toggle menu.
  • JavaScript tối giản: toggle class hidden trên mobile menu + đổi icon.

1.6. BÀI TẬP 2 — Grid: 1 col Mobile → 2 cols Tablet → 3 cols Desktop → 4 cols Wide

Yêu cầu: Grid layout responsive với 8 card items:

  • Mobile: 1 cột
  • Tablet (md): 2 cột
  • Desktop (lg): 3 cột
  • Wide (xl): 4 cột

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Grid - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="min-h-screen bg-gray-50 p-4 md:p-8 lg:p-12">

  <div class="max-w-7xl mx-auto">
    <h1 class="text-2xl md:text-3xl lg:text-4xl font-bold text-gray-900 mb-2">
      Responsive Grid Showcase
    </h1>
    <p class="text-gray-600 mb-8 md:mb-12">
      Resize cửa sổ trình duyệt để thấy grid thay đổi: 1 col → 2 cols → 3 cols → 4 cols
    </p>

    <!-- Grid responsive -->
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
      
      <!-- Card 1 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">Lightning Fast</h3>
        <p class="text-gray-600 text-sm">Build nhanh gấp 5x với Oxide Engine viết bằng Rust. Incremental build gần như tức thì.</p>
      </div>

      <!-- Card 2 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">CSS-first Config</h3>
        <p class="text-gray-600 text-sm">Không còn tailwind.config.js. Dùng @theme, @utility trực tiếp trong CSS với full IDE support.</p>
      </div>

      <!-- Card 3 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">OKLCH Colors</h3>
        <p class="text-gray-600 text-sm">Color space OKLCH mặc định — perceptually uniform, P3 wide gamut support.</p>
      </div>

      <!-- Card 4 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">Auto Content Detection</h3>
        <p class="text-gray-600 text-sm">Không cần cấu hình content path. Tailwind tự scan và generate đúng class bạn dùng.</p>
      </div>

      <!-- Card 5 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-pink-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-pink-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">Container Queries</h3>
        <p class="text-gray-600 text-sm">@container built-in — component tự thích nghi theo kích thước container, không phải viewport.</p>
      </div>

      <!-- Card 6 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-teal-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">3D Transforms</h3>
        <p class="text-gray-600 text-sm">rotate-x-*, rotate-y-*, rotate-z-* built-in. Transform 3D không cần plugin.</p>
      </div>

      <!-- Card 7 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">Radial & Conic Gradients</h3>
        <p class="text-gray-600 text-sm">bg-radial-* và bg-conic-* cho gradient đa dạng hơn, không chỉ linear.</p>
      </div>

      <!-- Card 8 -->
      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
        <div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 mb-2">V4.3 Scrollbar Styling</h3>
        <p class="text-gray-600 text-sm">scrollbar-thin, scrollbar-thumb-*, scrollbar-track-* — style scrollbar không cần plugin.</p>
      </div>

    </div>

    <!-- Breakpoint indicator (dev helper) -->
    <div class="fixed bottom-4 right-4 bg-gray-900 text-white text-xs px-3 py-1.5 rounded-full font-mono">
      <span class="sm:hidden">xs (&lt;640px)</span>
      <span class="hidden sm:inline md:hidden">sm (≥640px)</span>
      <span class="hidden md:inline lg:hidden">md (≥768px)</span>
      <span class="hidden lg:inline xl:hidden">lg (≥1024px)</span>
      <span class="hidden xl:inline 2xl:hidden">xl (≥1280px)</span>
      <span class="hidden 2xl:inline">2xl (≥1536px)</span>
    </div>
  </div>

</body>
</html>

Giải thích key decisions:

  • grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4: Quy luật mobile-first rõ ràng — tăng dần số cột.
  • gap-4 md:gap-6 lg:gap-8: Gap cũng tăng theo màn hình — khoảng cách giữa các card phù hợp với không gian có sẵn.
  • hover:shadow-md transition-shadow: Micro-interaction — card nổi lên khi hover.
  • Breakpoint indicator góc phải dưới: công cụ dev để biết đang ở breakpoint nào — dùng hidden/inline kết hợp prefix để show/hide.
  • Mỗi card có icon trong colored box + title + description — pattern cực phổ biến cho feature grid.

2. Container Queries (v4.0 NEW)

Container queries là một trong những tính năng quan trọng nhất của CSS hiện đại và Tailwind CSS v4 hỗ trợ built-in. Không cần plugin @tailwindcss/container-queries như v3 nữa.

2.1. Container Queries là gì?

Media queries respond theo viewport (kích thước cửa sổ trình duyệt). Container queries respond theo kích thước của container cha (phần tử chứa component).

Tại sao container queries quan trọng? Cùng một component card có thể được đặt trong sidebar hẹp (300px) hoặc trong main content rộng (800px). Với media query, bạn chỉ có 1 style cho tất cả. Với container query, component tự thích nghi dựa trên không gian nó thực sự có — tạo ra component thực sự reusable.

2.2. Cú pháp Container Queries trong Tailwind CSS v4

<!-- Bước 1: Đánh dấu container với class @container -->
<div class="@container">
  <!-- Bước 2: Dùng @sm:, @md:, @lg:, @xl: prefix cho children -->
  <div class="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3">
    <!-- Nội dung thay đổi dựa trên kích thước CONTAINER, không phải viewport -->
  </div>
</div>

Container query breakpoints mặc định (dựa trên width của container):

PrefixContainer width
@xs:≥ 20rem (320px)
@sm:≥ 24rem (384px)
@md:≥ 28rem (448px)
@lg:≥ 32rem (512px)
@xl:≥ 36rem (576px)
@2xl:≥ 42rem (672px)
@3xl:≥ 48rem (768px)
@4xl:≥ 56rem (896px)
@5xl:≥ 64rem (1024px)
@6xl:≥ 72rem (1152px)
@7xl:≥ 80rem (1280px)
Phân biệt rõ: md: (media query — viewport ≥ 768px) vs @md: (container query — container ≥ 448px). Một bên là chữ md:, một bên có dấu @.

2.3. Container Size Queries (v4.3 NEW)

V4.3 giới thiệu @container-size cho phép container queries dựa trên cả width và height, kèm theo units cqb (container query block = height) và cqh (container query height):

<div class="@container @container-size">
  <div class="@sm:flex-col @lg:flex-row">
    <!-- Layout thay đổi dựa trên kích thước container -->
  </div>
  <!-- Dùng đơn vị container query -->
  <p class="text-[cqb]">Text size dựa trên height của container</p>
</div>

2.4. BÀI TẬP 3 — Card thích nghi theo Container Width

Yêu cầu: Tạo một card component dùng container queries:

  • Khi container < 400px: layout dọc (image trên, text dưới), chữ nhỏ.
  • Khi container 400–600px: layout ngang (image trái, text phải), chữ vừa.
  • Khi container > 600px: layout ngang, image to hơn, text lớn hơn.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Container Queries Card - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="min-h-screen bg-gray-100 p-4 md:p-8">

  <div class="max-w-6xl mx-auto">
    <h1 class="text-2xl md:text-3xl font-bold text-gray-900 mb-8">
      Container Queries Demo — Resize các container bên dưới
    </h1>

    <p class="text-gray-600 mb-8">
      Cùng một card component, nhưng layout tự thay đổi theo kích thước CONTAINER CHA (không phải viewport).
      Kéo góc phải dưới của mỗi container để resize.
    </p>

    <!-- Container nhỏ (~320px) -->
    <div class="mb-8">
      <h2 class="text-sm font-medium text-gray-500 mb-2">Container ~320px (sidebar)</h2>
      <div class="@container border-2 border-dashed border-gray-300 rounded-lg p-4 resize-x overflow-auto min-w-[280px] max-w-[360px]" style="width: 320px">
        <!-- CARD COMPONENT -->
        <article class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden
                        grid @md:grid-cols-[120px_1fr] @lg:grid-cols-[200px_1fr]">
          <!-- Image -->
          <div class="bg-gradient-to-br from-blue-400 to-purple-500 
                      aspect-video @md:aspect-auto @md:h-full
                      @lg:aspect-auto @lg:h-full
                      flex items-center justify-center">
            <svg class="w-12 h-12 @lg:w-16 @lg:h-16 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
            </svg>
          </div>
          <!-- Content -->
          <div class="p-3 @lg:p-5">
            <span class="text-xs @lg:text-sm font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full">
              Technology
            </span>
            <h3 class="text-sm @lg:text-lg font-semibold text-gray-900 mt-2 @lg:mt-3 leading-snug">
              Building Responsive Components with Container Queries
            </h3>
            <p class="text-xs @lg:text-sm text-gray-600 mt-1 @lg:mt-2 line-clamp-2 @lg:line-clamp-2">
              Container queries cho phép component tự thích nghi dựa trên không gian thực tế nó có, không phụ thuộc vào viewport.
            </p>
          </div>
        </article>
      </div>
    </div>

    <!-- Container vừa (~500px) -->
    <div class="mb-8">
      <h2 class="text-sm font-medium text-gray-500 mb-2">Container ~500px (main content 1 cột)</h2>
      <div class="@container border-2 border-dashed border-gray-300 rounded-lg p-4 resize-x overflow-auto min-w-[400px] max-w-[550px]" style="width: 500px">
        <!-- CARD COMPONENT — IDENTICAL to above! -->
        <article class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden
                        grid @md:grid-cols-[120px_1fr] @lg:grid-cols-[200px_1fr]">
          <div class="bg-gradient-to-br from-blue-400 to-purple-500 
                      aspect-video @md:aspect-auto @md:h-full
                      @lg:aspect-auto @lg:h-full
                      flex items-center justify-center">
            <svg class="w-12 h-12 @lg:w-16 @lg:h-16 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
            </svg>
          </div>
          <div class="p-3 @lg:p-5">
            <span class="text-xs @lg:text-sm font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full">
              Technology
            </span>
            <h3 class="text-sm @lg:text-lg font-semibold text-gray-900 mt-2 @lg:mt-3 leading-snug">
              Building Responsive Components with Container Queries
            </h3>
            <p class="text-xs @lg:text-sm text-gray-600 mt-1 @lg:mt-2 line-clamp-2">
              Container queries cho phép component tự thích nghi dựa trên không gian thực tế nó có, không phụ thuộc vào viewport.
            </p>
          </div>
        </article>
      </div>
    </div>

    <!-- Container lớn (~750px) -->
    <div class="mb-8">
      <h2 class="text-sm font-medium text-gray-500 mb-2">Container ~750px (full width)</h2>
      <div class="@container border-2 border-dashed border-gray-300 rounded-lg p-4 resize-x overflow-auto min-w-[600px] max-w-[800px]" style="width: 750px">
        <!-- CARD COMPONENT — IDENTICAL to above! -->
        <article class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden
                        grid @md:grid-cols-[120px_1fr] @lg:grid-cols-[200px_1fr]">
          <div class="bg-gradient-to-br from-blue-400 to-purple-500 
                      aspect-video @md:aspect-auto @md:h-full
                      @lg:aspect-auto @lg:h-full
                      flex items-center justify-center">
            <svg class="w-12 h-12 @lg:w-16 @lg:h-16 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
            </svg>
          </div>
          <div class="p-3 @lg:p-5">
            <span class="text-xs @lg:text-sm font-medium text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full">
              Technology
            </span>
            <h3 class="text-sm @lg:text-lg font-semibold text-gray-900 mt-2 @lg:mt-3 leading-snug">
              Building Responsive Components with Container Queries
            </h3>
            <p class="text-xs @lg:text-sm text-gray-600 mt-1 @lg:mt-2 line-clamp-2">
              Container queries cho phép component tự thích nghi dựa trên không gian thực tế nó có, không phụ thuộc vào viewport. Điều này giúp tạo ra component thực sự reusable trong mọi ngữ cảnh layout.
            </p>
          </div>
        </article>
      </div>
    </div>

  </div>

</body>
</html>

Giải thích key decisions:

  • Cùng một HTML/CSS cho cả 3 container. Sự khác biệt đến từ container queries, không phải code khác nhau.
  • resize-x overflow-auto: Cho phép người dùng kéo để resize container và thấy component thay đổi.
  • grid @md:grid-cols-[120px_1fr] @lg:grid-cols-[200px_1fr]: Dùng container query để chuyển từ layout dọc sang layout ngang, và tăng kích thước ảnh.
  • @md:aspect-auto @md:h-full: Khi container đủ rộng, ảnh bỏ aspect-video và fill chiều cao card.
  • @lg:w-16 @lg:h-16: Icon to hơn khi container rộng hơn.
  • text-sm @lg:text-lg: Font size tăng theo container, không theo viewport.

2.5. BÀI TẬP 4 — Sidebar Widget thích nghi theo Available Space

Yêu cầu: Widget trong sidebar:

  • Container rộng (> 400px): layout ngang (icon trái, text phải).
  • Container hẹp (≤ 400px): layout dọc (icon trên, text dưới), text nhỏ hơn.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sidebar Widget Container Query - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="min-h-screen bg-gray-100 p-4 md:p-8">

  <div class="max-w-6xl mx-auto">
    <h1 class="text-2xl md:text-3xl font-bold text-gray-900 mb-2">
      Container Queries — Sidebar Widget
    </h1>
    <p class="text-gray-600 mb-8">
      Widget tự chuyển từ horizontal sang vertical layout dựa trên available space.
      Kéo cạnh phải của sidebar để resize.
    </p>

    <!-- Layout: Sidebar + Content -->
    <div class="flex gap-6">
      
      <!-- Sidebar (resizable) -->
      <aside class="resize-x overflow-auto min-w-[200px] max-w-[500px] border-2 border-dashed border-gray-300 rounded-lg p-4 bg-gray-50" style="width: 300px">
        
        <h2 class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-4">Sidebar Widgets</h2>

        <!-- Widget 1: Weather -->
        <div class="@container mb-4">
          <div class="bg-white rounded-lg border border-gray-200 p-3
                      @sm:flex @sm:items-center @sm:gap-3">
            <!-- Icon -->
            <div class="flex justify-center @sm:flex-shrink-0 mb-2 @sm:mb-0">
              <div class="w-10 h-10 @sm:w-12 @sm:h-12 bg-yellow-100 rounded-full flex items-center justify-center">
                <svg class="w-5 h-5 @sm:w-6 @sm:h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
                </svg>
              </div>
            </div>
            <!-- Text -->
            <div class="text-center @sm:text-left">
              <p class="font-semibold text-gray-900 text-sm @sm:text-base">28&deg;C Nắng</p>
              <p class="text-xs text-gray-500">Hồ Chí Minh, Việt Nam</p>
            </div>
          </div>
        </div>

        <!-- Widget 2: Recent Posts -->
        <div class="@container mb-4">
          <div class="bg-white rounded-lg border border-gray-200 p-3
                      @sm:flex @sm:items-center @sm:gap-3">
            <div class="flex justify-center @sm:flex-shrink-0 mb-2 @sm:mb-0">
              <div class="w-10 h-10 @sm:w-12 @sm:h-12 bg-blue-100 rounded-full flex items-center justify-center">
                <svg class="w-5 h-5 @sm:w-6 @sm:h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
                </svg>
              </div>
            </div>
            <div class="text-center @sm:text-left">
              <p class="font-semibold text-gray-900 text-sm @sm:text-base">12 Posts</p>
              <p class="text-xs text-gray-500">3 drafts pending</p>
            </div>
          </div>
        </div>

        <!-- Widget 3: Tasks -->
        <div class="@container mb-4">
          <div class="bg-white rounded-lg border border-gray-200 p-3
                      @sm:flex @sm:items-center @sm:gap-3">
            <div class="flex justify-center @sm:flex-shrink-0 mb-2 @sm:mb-0">
              <div class="w-10 h-10 @sm:w-12 @sm:h-12 bg-green-100 rounded-full flex items-center justify-center">
                <svg class="w-5 h-5 @sm:w-6 @sm:h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
                </svg>
              </div>
            </div>
            <div class="text-center @sm:text-left">
              <p class="font-semibold text-gray-900 text-sm @sm:text-base">8/12 Tasks</p>
              <p class="text-xs text-gray-500">67% completed</p>
            </div>
          </div>
        </div>

      </aside>

      <!-- Main Content -->
      <main class="flex-1 bg-white rounded-lg border border-gray-200 p-6">
        <h2 class="text-xl font-bold text-gray-900 mb-4">Main Content Area</h2>
        <p class="text-gray-600 mb-4">
          Kéo cạnh phải của sidebar (đường dashed) để resize. 
          Các widget trong sidebar sẽ tự động chuyển đổi layout dựa trên available width.
        </p>
        <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
          <h3 class="font-semibold text-gray-900 mb-2">Cách hoạt động:</h3>
          <ul class="list-disc list-inside text-sm text-gray-600 space-y-1">
            <li>Container <code class="bg-gray-200 px-1 rounded">@container</code> trên mỗi widget cha</li>
            <li>Prefix <code class="bg-gray-200 px-1 rounded">@sm:</code> để detect container width ≥ 384px</li>
            <li>Dưới 384px: layout dọc (icon trên, text dưới, text-center)</li>
            <li>Từ 384px: layout ngang (icon trái, text phải, text-left, icon to hơn)</li>
            <li>Không dùng media query — chỉ dùng container query!</li>
          </ul>
        </div>
      </main>

    </div>
  </div>

</body>
</html>

Giải thích key decisions:

  • @container trên mỗi widget wrapper để tạo container query context.
  • @sm:flex @sm:items-center @sm:gap-3: Khi container đủ rộng (≥ 384px), chuyển sang flex row.
  • @sm:w-12 @sm:h-12: Icon to hơn khi có nhiều không gian.
  • text-center @sm:text-left: Text căn giữa khi hẹp, căn trái khi rộng.
  • resize-x overflow-auto trên sidebar để demo tương tác.

3. Dark Mode

Dark mode không còn là "nice to have" — nó là tiêu chuẩn mong đợi cho mọi ứng dụng web hiện đại. Tailwind CSS v4 hỗ trợ dark mode qua variant dark:.

3.1. Cách dark: hoạt động

dark: variant hoạt động dựa trên CSS @media (prefers-color-scheme: dark) hoặc class dark trên phần tử cha (thường là <html>):

<!-- Dựa trên system preference (media query) — mặc định -->
<div class="bg-white dark:bg-gray-900">
  <p class="text-gray-900 dark:text-white">Nội dung</p>
</div>

<!-- Dựa trên class "dark" (manual toggle) — cần config -->
<html class="dark">
  <!-- ... -->
</html>

3.2. Configuration Dark Mode trong v4 (CSS-first)

@import "tailwindcss";

/* Mặc định: dark mode dựa trên media query (system preference) */
/* Không cần config gì thêm */

/* Để chuyển sang class-based (manual toggle): */
@custom-variant dark (&:where(.dark, .dark *));
Media vs Class strategy:
  • Media (mặc định): Theo system preference của OS. Đơn giản, không cần JS. Nhưng user không toggle được trong app.
  • Class-based: Bạn control bằng JavaScript. User có thể toggle trong app, preference lưu vào localStorage. Phổ biến hơn trong thực tế.

3.3. Dark Mode Color Patterns

Khi thiết kế dark mode, không chỉ đơn giản là đảo màu. Đây là pattern phổ biến:

ElementLight ModeDark Mode
Background chínhbg-white hoặc bg-gray-50bg-gray-900 hoặc bg-gray-950
Surface (card, modal)bg-whitebg-gray-800
Borderborder-gray-200border-gray-700
Text chínhtext-gray-900text-gray-100 hoặc text-white
Text phụtext-gray-600text-gray-400
Text mutedtext-gray-400text-gray-500
<!-- Pattern: Card component với dark mode -->
<div class="bg-white dark:bg-gray-800 
            border border-gray-200 dark:border-gray-700 
            rounded-xl p-6 shadow-sm">
  <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
    Card Title
  </h3>
  <p class="text-gray-600 dark:text-gray-400 mt-2">
    Card description with muted text that adapts to dark mode.
  </p>
  <button class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg 
                 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 
                 transition-colors">
    Action
  </button>
</div>

3.4. BÀI TẬP 5 — Full Dark Mode Toggle (Button + JavaScript Logic)

Yêu cầu: Trang web có dark mode toggle:

  • Nút toggle ở góc phải trên (icon mặt trời/mặt trăng).
  • Mặc định theo system preference.
  • User click toggle → ghi đè preference, lưu vào localStorage.
  • Animation mượt khi chuyển đổi.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi" class="">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dark Mode Toggle - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
  <style>
    /* Custom variant cho class-based dark mode */
    @custom-variant dark (&:where(.dark, .dark *));
    
    /* Transition cho toàn bộ color changes */
    *, *::before, *::after {
      transition: background-color 0.3s ease, border-color 0.3s ease, color 0.2s ease;
    }
  </style>
</head>
<body class="min-h-screen bg-gray-50 dark:bg-gray-950 transition-colors">

  <div class="max-w-4xl mx-auto px-4 py-8">
    
    <!-- Header với Toggle -->
    <div class="flex items-center justify-between mb-12">
      <h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white">
        Dark Mode Demo
      </h1>
      
      <!-- Toggle Button -->
      <button id="theme-toggle" 
              class="relative inline-flex h-10 w-10 items-center justify-center rounded-lg 
                     bg-white dark:bg-gray-800 
                     border border-gray-200 dark:border-gray-700
                     hover:bg-gray-100 dark:hover:bg-gray-700
                     transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
              aria-label="Toggle dark mode">
        <!-- Sun icon (hiện trong dark mode) -->
        <svg id="sun-icon" class="hidden dark:block w-5 h-5 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
        </svg>
        <!-- Moon icon (hiện trong light mode) -->
        <svg id="moon-icon" class="block dark:hidden w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
        </svg>
      </button>
    </div>

    <!-- Content Cards -->
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
      
      <!-- Card 1 -->
      <div class="bg-white dark:bg-gray-800 
                  border border-gray-200 dark:border-gray-700 
                  rounded-xl p-6 shadow-sm">
        <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
          System Preference Detection
        </h2>
        <p class="text-gray-600 dark:text-gray-400 mb-4">
          Trang web tự động detect system preference của bạn qua 
          <code class="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded text-sm">prefers-color-scheme</code>.
        </p>
        <div class="flex items-center gap-2 text-sm">
          <span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-full font-medium">
            Auto-detected
          </span>
        </div>
      </div>

      <!-- Card 2 -->
      <div class="bg-white dark:bg-gray-800 
                  border border-gray-200 dark:border-gray-700 
                  rounded-xl p-6 shadow-sm">
        <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
          Persistent Preference
        </h2>
        <p class="text-gray-600 dark:text-gray-400 mb-4">
          Lựa chọn của bạn được lưu vào <code class="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded text-sm">localStorage</code> 
          và khôi phục khi quay lại trang.
        </p>
        <div class="flex items-center gap-2 text-sm">
          <span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 rounded-full font-medium">
            localStorage
          </span>
        </div>
      </div>

    </div>

    <!-- Code Block Demo -->
    <div class="bg-white dark:bg-gray-800 
                border border-gray-200 dark:border-gray-700 
                rounded-xl p-6 shadow-sm">
      <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
        Code Preview
      </h2>
      <pre class="bg-gray-50 dark:bg-gray-900 
                  border border-gray-200 dark:border-gray-700 
                  rounded-lg p-4 overflow-x-auto">
<code class="text-sm text-gray-800 dark:text-gray-300"><span class="text-purple-600 dark:text-purple-400">function</span> <span class="text-blue-600 dark:text-blue-400">toggleDarkMode</span>() {
  <span class="text-gray-500 dark:text-gray-500">// Toggle class 'dark' trên <html></span>
  document.documentElement.classList.<span class="text-green-600 dark:text-green-400">toggle</span>(<span class="text-orange-600 dark:text-orange-400">'dark'</span>);
  
  <span class="text-gray-500 dark:text-gray-500">// Lưu preference</span>
  <span class="text-purple-600 dark:text-purple-400">const</span> isDark = document.documentElement.classList.<span class="text-green-600 dark:text-green-400">contains</span>(<span class="text-orange-600 dark:text-orange-400">'dark'</span>);
  localStorage.<span class="text-green-600 dark:text-green-400">setItem</span>(<span class="text-orange-600 dark:text-orange-400">'theme'</span>, isDark ? <span class="text-orange-600 dark:text-orange-400">'dark'</span> : <span class="text-orange-600 dark:text-orange-400">'light'</span>);
}</code>
      </pre>
    </div>

  </div>

  <script>
    const themeToggle = document.getElementById('theme-toggle');
    const html = document.documentElement;

    // Hàm set theme
    function setTheme(theme) {
      if (theme === 'dark') {
        html.classList.add('dark');
      } else {
        html.classList.remove('dark');
      }
      localStorage.setItem('theme', theme);
    }

    // Hàm get initial theme
    function getInitialTheme() {
      // 1. Check localStorage (user đã chọn trước đó)
      const savedTheme = localStorage.getItem('theme');
      if (savedTheme) return savedTheme;
      
      // 2. Check system preference
      if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
        return 'dark';
      }
      
      // 3. Default light
      return 'light';
    }

    // Khởi tạo theme
    const currentTheme = getInitialTheme();
    setTheme(currentTheme);

    // Toggle khi click
    themeToggle.addEventListener('click', () => {
      const newTheme = html.classList.contains('dark') ? 'light' : 'dark';
      setTheme(newTheme);
    });

    // Lắng nghe system preference thay đổi (chỉ khi user chưa chọn thủ công)
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
      // Chỉ auto-switch nếu user chưa set thủ công
      if (!localStorage.getItem('theme')) {
        setTheme(e.matches ? 'dark' : 'light');
      }
    });
  </script>

</body>
</html>

Giải thích key decisions:

  • @custom-variant dark (&:where(.dark, .dark *));: Cấu hình dark mode dạng class-based qua CSS (v4 style).
  • Transition trên *: Tất cả color changes có animation mượt 0.3s.
  • hidden dark:block / block dark:hidden: Icon mặt trời/mặt trăng tự đổi.
  • localStorage: Lưu preference để lần sau vào trang vẫn giữ.
  • matchMedia('(prefers-color-scheme: dark)'): Detect system preference làm default.
  • matchMedia(...).addEventListener: Lắng nghe khi user đổi system preference (ví dụ: macOS auto-switch buổi tối).

3.5. BÀI TẬP 6 — Card Component với Light/Dark Variants Inline

Yêu cầu: Tạo card component mà mọi style cho cả light và dark mode đều được định nghĩa inline trong HTML, không cần CSS riêng.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi" class="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Card Dark Mode Inline - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
  <style>
    @custom-variant dark (&:where(.dark, .dark *));
  </style>
</head>
<body class="min-h-screen bg-gray-100 dark:bg-gray-950 p-4 md:p-8 transition-colors">

  <div class="max-w-5xl mx-auto">
    
    <div class="flex items-center justify-between mb-8">
      <h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white">
        Card Components — Light & Dark Inline
      </h1>
      <button onclick="document.documentElement.classList.toggle('dark')" 
              class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-lg text-sm font-medium hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
        Toggle Theme
      </button>
    </div>

    <!-- Card Grid -->
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      
      <!-- Card 1: Default -->
      <div class="group bg-white dark:bg-gray-800 
                  border border-gray-200 dark:border-gray-700 
                  rounded-2xl overflow-hidden
                  shadow-sm hover:shadow-lg dark:shadow-gray-900/50
                  transition-all duration-300">
        <!-- Image -->
        <div class="h-48 bg-gradient-to-br from-blue-400 to-blue-600 
                    dark:from-blue-600 dark:to-blue-900 
                    flex items-center justify-center">
          <svg class="w-16 h-16 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/>
          </svg>
        </div>
        <!-- Content -->
        <div class="p-5">
          <div class="flex items-center gap-2 mb-3">
            <span class="text-xs font-medium px-2 py-1 rounded-full 
                         bg-blue-100 text-blue-700 
                         dark:bg-blue-900/40 dark:text-blue-300">
              Feature
            </span>
            <span class="text-xs text-gray-400 dark:text-gray-500">5 min read</span>
          </div>
          <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2 
                     group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
            Lightning Fast Builds
          </h3>
          <p class="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
            Oxide Engine viết bằng Rust giúp build nhanh gấp 5x. Incremental build gần như tức thì trong development.
          </p>
          <div class="flex items-center justify-between mt-4 pt-4 
                      border-t border-gray-100 dark:border-gray-700">
            <div class="flex items-center gap-2">
              <div class="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-600"></div>
              <span class="text-sm text-gray-700 dark:text-gray-300">Adam Wathan</span>
            </div>
            <button class="text-sm font-medium text-blue-600 dark:text-blue-400 
                           hover:text-blue-700 dark:hover:text-blue-300 transition-colors">
              Read more →
            </button>
          </div>
        </div>
      </div>

      <!-- Card 2: With stats -->
      <div class="group bg-white dark:bg-gray-800 
                  border border-gray-200 dark:border-gray-700 
                  rounded-2xl overflow-hidden
                  shadow-sm hover:shadow-lg dark:shadow-gray-900/50
                  transition-all duration-300">
        <div class="h-48 bg-gradient-to-br from-purple-400 to-pink-500 
                    dark:from-purple-700 dark:to-pink-800 
                    flex items-center justify-center">
          <svg class="w-16 h-16 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
          </svg>
        </div>
        <div class="p-5">
          <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
            Analytics Dashboard
          </h3>
          <div class="grid grid-cols-2 gap-3 mb-4">
            <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 text-center">
              <p class="text-2xl font-bold text-gray-900 dark:text-white">12.5K</p>
              <p class="text-xs text-gray-500 dark:text-gray-400">Users</p>
            </div>
            <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 text-center">
              <p class="text-2xl font-bold text-gray-900 dark:text-white">89%</p>
              <p class="text-xs text-gray-500 dark:text-gray-400">Satisfaction</p>
            </div>
          </div>
          <button class="w-full py-2 bg-purple-600 dark:bg-purple-500 text-white rounded-lg 
                         hover:bg-purple-700 dark:hover:bg-purple-600 
                         transition-colors text-sm font-medium">
            View Dashboard
          </button>
        </div>
      </div>

      <!-- Card 3: Minimal -->
      <div class="group bg-white dark:bg-gray-800 
                  border border-gray-200 dark:border-gray-700 
                  rounded-2xl overflow-hidden
                  shadow-sm hover:shadow-lg dark:shadow-gray-900/50
                  transition-all duration-300">
        <div class="h-48 bg-gradient-to-br from-green-400 to-teal-500 
                    dark:from-green-700 dark:to-teal-800 
                    flex items-center justify-center">
          <svg class="w-16 h-16 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
          </svg>
        </div>
        <div class="p-5">
          <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
            CSS-first Configuration
          </h3>
          <p class="text-gray-600 dark:text-gray-400 text-sm leading-relaxed mb-4">
            Không còn tailwind.config.js. Mọi thứ được cấu hình trực tiếp trong CSS với @theme và @utility.
          </p>
          <div class="flex items-center gap-1 text-yellow-500">
            <svg class="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
            <svg class="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
            <svg class="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
            <svg class="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
            <svg class="w-4 h-4 fill-current text-gray-300 dark:text-gray-600" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
          </div>
        </div>
      </div>

    </div>
  </div>

</body>
</html>

Giải thích key decisions:

  • Mỗi card có cặp class light/dark cho MỌI element: background, border, text, shadow, gradient.
  • group + group-hover:: Hiệu ứng hover đồng bộ trên cả card (title đổi màu khi hover card).
  • shadow-sm hover:shadow-lg dark:shadow-gray-900/50: Shadow dark mode dùng màu tối hơn để shadow vẫn visible trên nền tối.
  • Star rating dùng SVG inline, sao cuối có text-gray-300 dark:text-gray-600 cho "empty star" state.
  • transition-all duration-300: Mọi thay đổi (bao gồm dark mode switch) có animation.

4. Custom Variants (v4.0)

Tailwind CSS v4 cho phép bạn tự định nghĩa variant selector của riêng mình qua @custom-variant.

4.1. @custom-variant Cơ Bản

@import "tailwindcss";

/* Định nghĩa custom variant */
@custom-variant theme-a (&:where(.theme-a, .theme-a *));
@custom-variant theme-b (&:where(.theme-b, .theme-b *));

/* Sử dụng trong HTML */
/* <div class="bg-white theme-a:bg-blue-50 theme-b:bg-green-50"> */

4.2. Stacked Variants (v4.3)

V4.3 cho phép định nghĩa variant lồng nhau:

/* Stacked: apply styles when element is BOTH hovered AND focused */
@variant hover:focus {
  .btn-primary {
    background-color: var(--color-primary-dark);
    transform: scale(0.98);
  }
}

4.3. Compound Variants (v4.3)

/* Compound: apply styles when element is EITHER hovered OR focused */
@variant hover, focus {
  .btn-primary {
    box-shadow: 0 0 0 3px var(--color-primary/30%);
  }
}

4.4. BÀI TẬP 7 — Custom theme- Variant cho Multi-Theme Support

Yêu cầu: Tạo 3 theme (default, forest, ocean) với custom variant theme-*. User có thể chọn theme qua button.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi" class="">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Multi-Theme với Custom Variants - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
  <style>
    @import "tailwindcss";
    
    /* Custom variants cho từng theme */
    @custom-variant theme-forest (&:where(.theme-forest, .theme-forest *));
    @custom-variant theme-ocean (&:where(.theme-ocean, .theme-ocean *));
    
    /* Base transitions */
    *, *::before, *::after {
      transition: background-color 0.3s ease, border-color 0.3s ease, color 0.2s ease;
    }
  </style>
</head>
<body class="min-h-screen bg-gray-50 theme-forest:bg-green-50 theme-ocean:bg-blue-50 p-4 md:p-8">

  <div class="max-w-3xl mx-auto">
    
    <h1 class="text-2xl md:text-3xl font-bold text-gray-900 
               theme-forest:text-green-900 theme-ocean:text-blue-900 mb-2">
      Multi-Theme với Custom Variants
    </h1>
    <p class="text-gray-600 theme-forest:text-green-700 theme-ocean:text-blue-700 mb-8">
      Chọn theme bên dưới. Tất cả component tự động đổi màu theo theme được chọn.
    </p>

    <!-- Theme Selector -->
    <div class="flex flex-wrap gap-3 mb-8">
      <button onclick="setTheme('')" 
              class="px-4 py-2 rounded-lg text-sm font-medium transition-all
                     bg-white border-2 border-gray-300 text-gray-700 hover:border-gray-400
                     theme-forest:border-green-200 theme-forest:text-green-700 theme-forest:bg-green-50
                     theme-ocean:border-blue-200 theme-ocean:text-blue-700 theme-ocean:bg-blue-50">
        ⚪ Default
      </button>
      <button onclick="setTheme('theme-forest')" 
              class="px-4 py-2 rounded-lg text-sm font-medium transition-all
                     bg-white border-2 border-green-300 text-green-700 hover:border-green-400
                     theme-forest:bg-green-600 theme-forest:text-white theme-forest:border-green-600
                     theme-ocean:border-blue-200 theme-ocean:text-blue-700">
        🌲 Forest
      </button>
      <button onclick="setTheme('theme-ocean')" 
              class="px-4 py-2 rounded-lg text-sm font-medium transition-all
                     bg-white border-2 border-blue-300 text-blue-700 hover:border-blue-400
                     theme-forest:border-green-200 theme-forest:text-green-700
                     theme-ocean:bg-blue-600 theme-ocean:text-white theme-ocean:border-blue-600">
        🌊 Ocean
      </button>
    </div>

    <!-- Card -->
    <div class="bg-white theme-forest:bg-green-100/50 theme-ocean:bg-blue-100/50
                border border-gray-200 theme-forest:border-green-300 theme-ocean:border-blue-300
                rounded-xl p-6 shadow-sm mb-6">
      <h2 class="text-lg font-semibold text-gray-900 
                 theme-forest:text-green-900 theme-ocean:text-blue-900 mb-3">
        Welcome to Multi-Theme
      </h2>
      <p class="text-gray-600 theme-forest:text-green-800 theme-ocean:text-blue-800">
        Card này tự động đổi màu nền, viền, chữ theo theme được chọn. Tất cả từ custom variant.
      </p>
      <button class="mt-4 px-4 py-2 rounded-lg text-sm font-medium text-white transition-all
                     bg-gray-900 hover:bg-gray-800
                     theme-forest:bg-green-700 theme-forest:hover:bg-green-800
                     theme-ocean:bg-blue-700 theme-ocean:hover:bg-blue-800">
        Action Button
      </button>
    </div>

    <!-- Stats -->
    <div class="grid grid-cols-3 gap-4 mb-6">
      <div class="bg-white theme-forest:bg-green-50 theme-ocean:bg-blue-50
                  border border-gray-200 theme-forest:border-green-200 theme-ocean:border-blue-200
                  rounded-xl p-4 text-center">
        <p class="text-2xl font-bold text-gray-900 
                  theme-forest:text-green-700 theme-ocean:text-blue-700">128</p>
        <p class="text-xs text-gray-500 
                  theme-forest:text-green-600 theme-ocean:text-blue-600">Projects</p>
      </div>
      <div class="bg-white theme-forest:bg-green-50 theme-ocean:bg-blue-50
                  border border-gray-200 theme-forest:border-green-200 theme-ocean:border-blue-200
                  rounded-xl p-4 text-center">
        <p class="text-2xl font-bold text-gray-900 
                  theme-forest:text-green-700 theme-ocean:text-blue-700">3.2K</p>
        <p class="text-xs text-gray-500 
                  theme-forest:text-green-600 theme-ocean:text-blue-600">Users</p>
      </div>
      <div class="bg-white theme-forest:bg-green-50 theme-ocean:bg-blue-50
                  border border-gray-200 theme-forest:border-green-200 theme-ocean:border-blue-200
                  rounded-xl p-4 text-center">
        <p class="text-2xl font-bold text-gray-900 
                  theme-forest:text-green-700 theme-ocean:text-blue-700">99.9%</p>
        <p class="text-xs text-gray-500 
                  theme-forest:text-green-600 theme-ocean:text-blue-600">Uptime</p>
      </div>
    </div>

    <!-- Alert -->
    <div class="bg-amber-50 theme-forest:bg-green-100 theme-ocean:bg-blue-100
                border border-amber-200 theme-forest:border-green-300 theme-ocean:border-blue-300
                rounded-xl p-4 flex items-start gap-3">
      <span class="text-amber-600 theme-forest:text-green-700 theme-ocean:text-blue-700 text-lg">💡</span>
      <div>
        <p class="font-medium text-amber-800 theme-forest:text-green-900 theme-ocean:text-blue-900">
          Custom Variant Power
        </p>
        <p class="text-sm text-amber-700 theme-forest:text-green-800 theme-ocean:text-blue-800 mt-1">
          Mỗi element có thể có style riêng cho từng theme — tất cả inline, không cần CSS file riêng.
          Dùng <code class="bg-amber-200/50 theme-forest:bg-green-200/50 theme-ocean:bg-blue-200/50 px-1 rounded">@custom-variant</code> trong CSS.
        </p>
      </div>
    </div>

  </div>

  <script>
    function setTheme(themeClass) {
      const html = document.documentElement;
      // Remove all theme classes
      html.classList.remove('theme-forest', 'theme-ocean');
      // Add selected theme
      if (themeClass) {
        html.classList.add(themeClass);
      }
      // Save preference
      localStorage.setItem('app-theme', themeClass);
    }

    // Restore theme on load
    const savedTheme = localStorage.getItem('app-theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  </script>

</body>
</html>

Giải thích key decisions:

  • @custom-variant theme-forest (&:where(.theme-forest, .theme-forest *));: Tạo variant mới kích hoạt khi phần tử nằm trong cây có class .theme-forest.
  • Mỗi element có 3 style: default + theme-forest: + theme-ocean:.
  • Theme selector button cũng đổi style theo theme đang active (active theme button có màu đậm).
  • localStorage: Lưu theme đã chọn.

5. Print Styles

Tailwind CSS v4 cung cấp print: variant để style khi in ấn:

<div class="bg-gray-100 print:bg-white shadow-lg print:shadow-none">
  <nav class="print:hidden"><!-- Ẩn navigation khi in --></nav>
  <main class="p-8 print:p-0">
    <h1 class="text-4xl print:text-2xl">Resume</h1>
    <table class="border print:border-black"><!-- Border rõ hơn khi in --></table>
  </main>
</div>

5.1. BÀI TẬP 8 — Print-Friendly Resume Layout

Yêu cầu: CV/resume layout:

  • Trên màn hình: thiết kế đẹp với màu sắc, shadow, navigation.
  • Khi in: ẩn navigation, tối ưu text size, bỏ shadow/background, hiện link URL.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Print-Friendly Resume - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
  <style>
    @media print {
      @page {
        margin: 1.5cm;
        size: A4;
      }
    }
  </style>
</head>
<body class="min-h-screen bg-gray-100 print:bg-white">

  <!-- Navigation (hidden when printing) -->
  <nav class="print:hidden bg-white border-b border-gray-200 sticky top-0 z-10">
    <div class="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
      <span class="font-bold text-gray-900">Resume Preview</span>
      <button onclick="window.print()" 
              class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
        🖨️ Print / Save PDF
      </button>
    </div>
  </nav>

  <!-- Resume Content -->
  <main class="max-w-3xl mx-auto my-8 print:my-0">
    <div class="bg-white shadow-lg print:shadow-none rounded-2xl print:rounded-none overflow-hidden">
      
      <!-- Header -->
      <header class="bg-gradient-to-r from-blue-600 to-purple-600 
                     print:bg-white print:border-b print:border-gray-300
                     px-8 py-10 print:px-0 print:py-6">
        <h1 class="text-4xl print:text-3xl font-bold text-white print:text-gray-900">
          Nguyen Van A
        </h1>
        <p class="text-blue-100 print:text-gray-600 text-lg mt-2 print:mt-1">
          Senior Frontend Developer
        </p>
        <div class="flex flex-wrap gap-4 mt-4 print:mt-2 text-sm text-blue-100 print:text-gray-600">
          <span>📧 email@example.com</span>
          <span>📱 +84 123 456 789</span>
          <span>📍 Ho Chi Minh City</span>
          <span class="print:hidden">🌐 portfolio.dev</span>
          <!-- Show URL when printing -->
          <span class="hidden print:inline">🔗 https://portfolio.dev</span>
        </div>
      </header>

      <!-- Body -->
      <div class="p-8 print:p-0 print:pt-6">
        
        <!-- Summary -->
        <section class="mb-8 print:mb-4">
          <h2 class="text-lg font-bold text-gray-900 uppercase tracking-wider 
                     border-b-2 border-blue-500 pb-2 mb-3">
            Professional Summary
          </h2>
          <p class="text-gray-700 print:text-gray-900 leading-relaxed">
            Frontend developer với 5 năm kinh nghiệm xây dựng web applications sử dụng React, Vue.js, và TypeScript. 
            Đam mê performance optimization, design systems, và developer experience.
          </p>
        </section>

        <!-- Experience -->
        <section class="mb-8 print:mb-4">
          <h2 class="text-lg font-bold text-gray-900 uppercase tracking-wider 
                     border-b-2 border-blue-500 pb-2 mb-3">
            Work Experience
          </h2>
          
          <div class="mb-4 print:mb-2">
            <div class="flex justify-between items-baseline">
              <h3 class="font-semibold text-gray-900">Senior Frontend Developer</h3>
              <span class="text-sm text-gray-500 print:text-gray-700">2023 — Present</span>
            </div>
            <p class="text-blue-600 print:text-gray-700 text-sm font-medium">Tech Company ABC</p>
            <ul class="list-disc list-inside text-gray-700 print:text-gray-900 text-sm mt-2 space-y-1">
              <li>Lead team 4 developers xây dựng design system với React + TypeScript</li>
              <li>Giảm 40% bundle size qua code splitting và tree shaking</li>
              <li>Migration từ CSS Modules sang Tailwind CSS — giảm 60% CSS bundle</li>
            </ul>
          </div>

          <div class="mb-4 print:mb-2">
            <div class="flex justify-between items-baseline">
              <h3 class="font-semibold text-gray-900">Frontend Developer</h3>
              <span class="text-sm text-gray-500 print:text-gray-700">2020 — 2023</span>
            </div>
            <p class="text-blue-600 print:text-gray-700 text-sm font-medium">Startup XYZ</p>
            <ul class="list-disc list-inside text-gray-700 print:text-gray-900 text-sm mt-2 space-y-1">
              <li>Xây dựng SPA với Vue 3 + Pinia — phục vụ 100K+ users</li>
              <li>Tích hợp CI/CD pipeline với GitHub Actions và Vercel</li>
              <li>Viết unit/integration test với Vitest — coverage 85%+</li>
            </ul>
          </div>
        </section>

        <!-- Skills -->
        <section class="mb-8 print:mb-4">
          <h2 class="text-lg font-bold text-gray-900 uppercase tracking-wider 
                     border-b-2 border-blue-500 pb-2 mb-3">
            Skills
          </h2>
          <div class="flex flex-wrap gap-2">
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">React</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">Vue.js</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">TypeScript</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">Tailwind CSS</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">Next.js</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">Nuxt</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">Node.js</span>
            <span class="px-3 py-1 bg-blue-100 text-blue-700 
                         print:bg-gray-100 print:text-gray-900 print:border print:border-gray-300
                         rounded-full text-sm font-medium">PostgreSQL</span>
          </div>
        </section>

        <!-- Education -->
        <section class="mb-8 print:mb-4">
          <h2 class="text-lg font-bold text-gray-900 uppercase tracking-wider 
                     border-b-2 border-blue-500 pb-2 mb-3">
            Education
          </h2>
          <div class="flex justify-between items-baseline">
            <div>
              <h3 class="font-semibold text-gray-900">Bachelor of Computer Science</h3>
              <p class="text-blue-600 print:text-gray-700 text-sm">University of Technology</p>
            </div>
            <span class="text-sm text-gray-500 print:text-gray-700">2016 — 2020</span>
          </div>
        </section>

      </div>
    </div>
  </main>

</body>
</html>

Giải thích key decisions:

  • print:hidden trên navigation bar — không ai muốn in navigation.
  • print:bg-white, print:shadow-none: Bỏ màu nền gradient và shadow khi in để tiết kiệm mực.
  • hidden print:inline: Hiện full URL khi in (trên màn hình chỉ hiện text rút gọn).
  • print:text-gray-900: Text đậm hơn khi in để dễ đọc trên giấy.
  • print:border print:border-gray-300: Thêm border cho tag skill để phân biệt khi không có màu nền.
  • @page { margin: 1.5cm; size: A4; }: CSS print media rule cho canh lề và khổ giấy.

6. Screen Readers & Accessibility

6.1. sr-onlynot-sr-only

<!-- Chỉ screen reader thấy -->
<span class="sr-only">Đang tải dữ liệu...</span>

<!-- Ẩn khỏi screen reader nhưng visible trên màn hình -->
<div class="not-sr-only">Nội dung trực quan</div>

6.2. motion-safe:motion-reduce:

<!-- Animation chỉ chạy khi user KHÔNG bật reduced motion -->
<div class="motion-safe:animate-spin">Loading spinner</div>

<!-- Style riêng cho user bật reduced motion -->
<div class="animate-bounce motion-reduce:animate-none">Thông báo</div>

Yêu cầu: Link "Skip to main content" chỉ hiện khi focus (tab), giúp keyboard user bỏ qua navigation.

Code đầy đủ:

<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Accessible Skip Link - Tailwind CSS v4</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="min-h-screen bg-gray-50">

  <!-- Skip to content link — chỉ hiện khi focus (Tab) -->
  <a href="#main-content" 
     class="sr-only focus:not-sr-only 
            focus:fixed focus:top-4 focus:left-4 
            focus:bg-blue-600 focus:text-white 
            focus:px-4 focus:py-2 focus:rounded-lg 
            focus:z-50 focus:shadow-lg
            focus:outline-none focus:ring-2 focus:ring-blue-300">
    Bỏ qua và đến nội dung chính
  </a>

  <!-- Navigation (có thể rất dài với nhiều link) -->
  <header class="bg-white border-b border-gray-200">
    <nav class="max-w-5xl mx-auto px-4 py-3" aria-label="Main navigation">
      <ul class="flex gap-6">
        <li><a href="#" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded px-1">Home</a></li>
        <li><a href="#" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded px-1">Products</a></li>
        <li><a href="#" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded px-1">Services</a></li>
        <li><a href="#" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded px-1">About</a></li>
        <li><a href="#" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded px-1">Contact</a></li>
      </ul>
    </nav>
  </header>

  <!-- Main content -->
  <main id="main-content" class="max-w-3xl mx-auto px-4 py-12" tabindex="-1">
    
    <h1 class="text-3xl font-bold text-gray-900 mb-4">Nội dung chính</h1>
    
    <p class="text-gray-600 mb-6">
      Nhấn <kbd class="px-2 py-0.5 bg-gray-200 rounded text-sm font-mono">Tab</kbd> khi trang vừa load. 
      Link "Bỏ qua và đến nội dung chính" sẽ xuất hiện ở góc trên bên trái.
      Nhấn <kbd class="px-2 py-0.5 bg-gray-200 rounded text-sm font-mono">Enter</kbd> để skip navigation.
    </p>

    <!-- Demo content với các interactive elements -->
    <div class="space-y-4">
      <div class="bg-white border border-gray-200 rounded-xl p-6">
        <h2 class="text-lg font-semibold text-gray-900 mb-3">Accessibility Features</h2>
        
        <ul class="space-y-3 text-sm text-gray-700">
          <li class="flex items-start gap-2">
            <span class="text-green-600 mt-0.5"></span>
            <span><strong>Skip link:</strong> Element đầu tiên trong DOM, giúp keyboard user skip navigation dài.</span>
          </li>
          <li class="flex items-start gap-2">
            <span class="text-green-600 mt-0.5"></span>
            <span><strong>Focus ring:</strong> Tất cả link có <code class="bg-gray-100 px-1 rounded">focus:ring-2</code> để visible focus indicator.</span>
          </li>
          <li class="flex items-start gap-2">
            <span class="text-green-600 mt-0.5"></span>
            <span><strong>aria-label:</strong> Navigation có <code class="bg-gray-100 px-1 rounded">aria-label="Main navigation"</code>.</span>
          </li>
          <li class="flex items-start gap-2">
            <span class="text-green-600 mt-0.5"></span>
            <span><strong>tabindex="-1":</strong> Main content có tabindex để focus sau khi skip.</span>
          </li>
        </ul>
      </div>

      <!-- Keyboard-test form -->
      <div class="bg-white border border-gray-200 rounded-xl p-6">
        <h2 class="text-lg font-semibold text-gray-900 mb-3">Test Keyboard Navigation</h2>
        <p class="text-sm text-gray-600 mb-4">Dùng Tab để di chuyển giữa các field bên dưới.</p>
        
        <form class="space-y-3" onsubmit="event.preventDefault();">
          <div>
            <label for="name" class="block text-sm font-medium text-gray-700 mb-1">Họ tên</label>
            <input id="name" type="text" 
                   class="w-full px-3 py-2 border border-gray-300 rounded-lg 
                          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
                          text-sm" 
                   placeholder="Nguyen Van A">
          </div>
          <div>
            <label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
            <input id="email" type="email" 
                   class="w-full px-3 py-2 border border-gray-300 rounded-lg 
                          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
                          text-sm" 
                   placeholder="email@example.com">
          </div>
          <button type="submit" 
                  class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium
                         hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
                         transition-colors">
            Submit
          </button>
        </form>
      </div>
    </div>

  </main>

</body>
</html>

Giải thích key decisions:

  • sr-only focus:not-sr-only: Link ẩn với mọi người, nhưng hiển thị khi nhận focus (Tab tới).
  • focus:fixed focus:top-4 focus:left-4: Khi hiện, link xuất hiện ở vị trí fixed trên màn hình.
  • id="main-content" + tabindex="-1": Cho phép focus vào main content sau khi skip (cần cho keyboard focus management).
  • focus:ring-2 focus:ring-blue-500 focus:ring-offset-2: Visible focus indicator trên tất cả interactive elements.
  • aria-label="Main navigation": Cung cấp context cho screen reader.
  • <kbd> styling: Hiển thị phím tắt đẹp mắt.

Tổng Kết

Responsive Design

Mobile-first với breakpoint prefix (sm:, md:, lg:, xl:, 2xl:). Mọi utility đều responsive được. Custom breakpoints qua @theme.

Container Queries

@container + @sm:, @md:, @lg: prefix. Component tự thích nghi theo container cha, không phải viewport. @container-size (v4.3) cho size queries.

Dark Mode

dark: variant với media query (mặc định) hoặc class-based. Toggle bằng JS + localStorage. Pattern màu sắc rõ ràng cho light/dark.

Custom Variants

@custom-variant để tạo variant riêng. Stacked variants (v4.3) và compound variants. Multi-theme support dễ dàng.

Print Styles

print: variant cho style khi in. Ẩn navigation, tối ưu text/color, hiện URL đầy đủ.

Accessibility

sr-only / not-sr-only, motion-safe: / motion-reduce:, focus ring, skip-to-content link, aria attributes.
Tiếp theo: 5. Customization & Theme — CSS-first config, @theme, @utility, @plugin và xây dựng design system hoàn chỉnh.

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

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