Tailwind CSS

Interactive States, Transitions & Animations

Tailwind CSS v4 — hover/focus/active states, transitions, transforms 3D, animations, filters, backdrop. Bài tập interactive components.

1. State Variants (hover:, focus:, active:, visited:, focus-visible:, focus-within:)

Mọi utility trong Tailwind đều có thể gắn với state variant — class chỉ áp dụng khi element ở trạng thái cụ thể.

<!-- Hover: đổi màu nền khi rê chuột -->
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg">
  Hover me
</button>

<!-- Focus: áp dụng khi element được focus -->
<input class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" />

<!-- Active: áp dụng khi đang click/giữ -->
<button class="bg-blue-500 active:bg-blue-700 active:scale-95 text-white px-4 py-2 rounded-lg">
  Click me
</button>

<!-- Visited: áp dụng cho link đã click -->
<a href="#" class="text-blue-600 visited:text-purple-600">Visited link</a>

<!-- focus-visible: chỉ hiện focus ring khi dùng keyboard (không phải mouse) -->
<button class="focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none">
  Keyboard accessible
</button>

<!-- focus-within: áp dụng khi bất kỳ child nào được focus -->
<div class="border border-gray-200 focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-200 rounded-lg p-4">
  <label class="block text-sm text-gray-500 focus-within:text-blue-600">Search</label>
  <input type="text" class="w-full outline-none" placeholder="Type to search..." />
</div>

group-hover / group-focus — Parent state affects children

<!-- Khi hover vào parent (.group), thay đổi child -->
<div class="group p-6 bg-white rounded-xl border border-gray-200 hover:shadow-lg transition-shadow cursor-pointer">
  <h3 class="text-lg font-semibold text-gray-900 group-hover:text-indigo-600 transition-colors">
    Tiêu đề card
  </h3>
  <p class="text-sm text-gray-500 mt-2 group-hover:text-gray-700 transition-colors">
    Mô tả cũng đổi màu khi hover vào toàn bộ card.
  </p>
  <svg class="w-5 h-5 text-gray-400 group-hover:text-indigo-500 group-hover:translate-x-1 transition-all mt-4">
    <!-- arrow icon -->
  </svg>
</div>

peer-* — Sibling state

<!-- Label thay đổi style khi input bên cạnh được focus -->
<label class="block">
  <span class="text-sm text-gray-500 peer-focus:text-blue-600">Email</span>
  <input type="email" class="peer mt-1 w-full border border-gray-300 rounded-lg px-3 py-2
                              focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none" />
</label>

disabled:, checked:, required:, invalid:, user-valid:/user-invalid:

<!-- Disabled state -->
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg
               disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed" disabled>
  Submit
</button>

<!-- Checked state (checkbox/radio) -->
<input type="checkbox" class="checked:bg-blue-500 checked:border-blue-500" />

<!-- Required indicator -->
<input required class="required:border-red-500" />

<!-- Invalid state (HTML5 validation) -->
<input type="email" class="invalid:border-red-500 invalid:text-red-600" />
NEW v4.0 — user-valid: / user-invalid:: Khác với valid: / invalid: (chạy ngay khi load), các variant này chỉ kích hoạt sau khi user đã tương tác với input. Đây là behavior chuẩn cho UX form hiện đại.
<input type="email" required
  class="border border-gray-300 rounded-lg px-3 py-2
         user-invalid:border-red-500 user-invalid:ring-2 user-invalid:ring-red-200
         user-valid:border-emerald-500 user-valid:ring-2 user-valid:ring-emerald-200" />

inert: (v4.0 NEW) và not-* (v4.0 NEW)

<!-- inert: — element được browser bỏ qua (không tương tác được) -->
<div class="inert:opacity-50 inert:cursor-not-allowed" inert>
  Nội dung bị vô hiệu hóa
</div>

<!-- not-* — áp dụng style khi KHÔNG ở trạng thái đó -->
<div class="not-disabled:bg-blue-500 not-disabled:hover:bg-blue-600 ...">
  Chỉ có style khi không disabled
</div>

Bài tập 1: Button với hover, active, focus-visible, disabled states

Yêu cầu: Thiết kế button system với đầy đủ 4 trạng thái tương tác.

<template>
  <div class="max-w-2xl mx-auto p-8 space-y-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-6">Button State System</h2>

    <!-- Primary button — đầy đủ states -->
    <div class="space-y-4">
      <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Primary Button</h3>

      <!-- Normal -->
      <button class="inline-flex items-center gap-2 px-5 py-2.5 bg-indigo-600 text-white text-sm font-semibold rounded-lg
                     hover:bg-indigo-700
                     active:bg-indigo-800 active:scale-[0.97]
                     focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2
                     disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:scale-100
                     transition-all duration-150">
        <span>Primary Action</span>
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7" />
        </svg>
      </button>

      <!-- Disabled -->
      <div>
        <button disabled class="inline-flex items-center gap-2 px-5 py-2.5 bg-gray-300 text-gray-500 text-sm font-semibold rounded-lg cursor-not-allowed transition-all duration-150">
          <span>Disabled State</span>
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7" />
          </svg>
        </button>
      </div>
    </div>

    <!-- Secondary / Outline button -->
    <div class="space-y-4">
      <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Secondary Button</h3>

      <button class="inline-flex items-center gap-2 px-5 py-2.5 border border-gray-300 text-gray-700 text-sm font-semibold rounded-lg
                     hover:bg-gray-50 hover:border-gray-400
                     active:bg-gray-100 active:scale-[0.97]
                     focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2
                     transition-all duration-150">
        Secondary Action
      </button>
    </div>

    <!-- Ghost / Text button -->
    <div class="space-y-4">
      <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Ghost Button</h3>

      <button class="inline-flex items-center gap-2 px-4 py-2 text-indigo-600 text-sm font-semibold rounded-lg
                     hover:bg-indigo-50
                     active:bg-indigo-100
                     focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500
                     transition-all duration-150">
        Ghost Action
      </button>
    </div>

    <!-- Danger button -->
    <div class="space-y-4">
      <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider">Danger Button</h3>

      <button class="inline-flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white text-sm font-semibold rounded-lg
                     hover:bg-red-700
                     active:bg-red-800 active:scale-[0.97]
                     focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2
                     transition-all duration-150">
        Delete Account
      </button>
    </div>
  </div>
</template>

Bài tập 2: Card reveals details on hover (group-hover)

Yêu cầu: Card sản phẩm — khi hover hiện overlay với nút "Thêm vào giỏ" và thông tin giá.

<template>
  <div class="max-w-xs mx-auto">
    <div class="group relative overflow-hidden rounded-2xl bg-white border border-gray-200 shadow-sm hover:shadow-lg transition-shadow">

      <!-- Ảnh sản phẩm -->
      <div class="aspect-square overflow-hidden bg-gray-100">
        <img
          src="https://images.unsplash.com/photo-1523275335684-37898b6baf30"
          alt="Product"
          class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
        />

        <!-- Overlay — hiện khi hover -->
        <div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
          <button class="px-6 py-2.5 bg-white text-gray-900 text-sm font-semibold rounded-lg
                         hover:bg-gray-100 transform translate-y-4 group-hover:translate-y-0
                         transition-all duration-300">
            Thêm vào giỏ
          </button>
        </div>

        <!-- Badge giảm giá -->
        <span class="absolute top-3 left-3 px-2.5 py-0.5 bg-red-500 text-white text-xs font-bold rounded-full">
          -30%
        </span>
      </div>

      <!-- Thông tin sản phẩm -->
      <div class="p-4">
        <p class="text-xs text-gray-400 uppercase tracking-wider">Thương hiệu</p>
        <h3 class="text-sm font-semibold text-gray-900 mt-1 group-hover:text-indigo-600 transition-colors">
          Tên sản phẩm cao cấp phiên bản giới hạn
        </h3>

        <div class="mt-3 flex items-center justify-between">
          <div>
            <span class="text-lg font-bold text-gray-900">790.000đ</span>
            <span class="ml-2 text-sm text-gray-400 line-through">1.150.000đ</span>
          </div>
          <div class="flex items-center gap-0.5">
            <span class="text-amber-400 text-sm">&#9733;</span>
            <span class="text-xs text-gray-500">4.8</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

Bài tập 3: Form input với validation states (valid/invalid)

Yêu cầu: Form đăng ký với real-time validation feedback sử dụng user-valid:user-invalid:.

<template>
  <div class="max-w-md mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Đăng ký tài khoản</h2>

    <form class="space-y-5" @submit.prevent>
      <!-- Full Name -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Họ và tên</label>
        <input
          type="text"
          required
          minlength="3"
          placeholder="Nguyen Van A"
          class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm
                 placeholder:text-gray-400
                 user-invalid:border-red-400 user-invalid:ring-2 user-invalid:ring-red-100
                 user-valid:border-emerald-400 user-valid:ring-2 user-valid:ring-emerald-100
                 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-400
                 transition-all duration-200"
        />
        <p class="mt-1 text-xs text-gray-400 hidden user-invalid:block peer-invalid:block">
          Vui lòng nhập ít nhất 3 ký tự
        </p>
      </div>

      <!-- Email -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Email</label>
        <input
          type="email"
          required
          placeholder="you@example.com"
          class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm
                 placeholder:text-gray-400
                 user-invalid:border-red-400 user-invalid:ring-2 user-invalid:ring-red-100
                 user-valid:border-emerald-400 user-valid:ring-2 user-valid:ring-emerald-100
                 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-400
                 transition-all duration-200"
        />
      </div>

      <!-- Password -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Mật khẩu</label>
        <input
          type="password"
          required
          minlength="8"
          placeholder="Ít nhất 8 ký tự"
          class="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm
                 placeholder:text-gray-400
                 user-invalid:border-red-400 user-invalid:ring-2 user-invalid:ring-red-100
                 user-valid:border-emerald-400 user-valid:ring-2 user-valid:ring-emerald-100
                 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-400
                 transition-all duration-200"
        />
        <!-- Password strength indicator -->
        <div class="mt-2 flex gap-1">
          <div class="h-1 flex-1 rounded-full bg-gray-200"></div>
          <div class="h-1 flex-1 rounded-full bg-gray-200"></div>
          <div class="h-1 flex-1 rounded-full bg-gray-200"></div>
          <div class="h-1 flex-1 rounded-full bg-gray-200"></div>
        </div>
      </div>

      <!-- Checkbox — checked style -->
      <label class="flex items-start gap-3 cursor-pointer group">
        <input
          type="checkbox"
          required
          class="mt-0.5 w-4 h-4 rounded border-gray-300 text-indigo-600
                 checked:bg-indigo-600 checked:border-indigo-600
                 focus:ring-2 focus:ring-indigo-200 focus:ring-offset-0"
        />
        <span class="text-sm text-gray-600 group-has-[:checked]:text-gray-900">
          Tôi đồng ý với <a href="#" class="text-indigo-600 hover:underline">Điều khoản dịch vụ</a>          <a href="#" class="text-indigo-600 hover:underline">Chính sách bảo mật</a>
        </span>
      </label>

      <button
        type="submit"
        class="w-full py-2.5 bg-indigo-600 text-white font-semibold rounded-lg
               hover:bg-indigo-700
               active:bg-indigo-800 active:scale-[0.98]
               focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2
               disabled:bg-gray-300 disabled:cursor-not-allowed
               transition-all duration-150">
        Tạo tài khoản
      </button>
    </form>
  </div>
</template>

Bài tập 4: Parent card highlights child icon on hover

Yêu cầu: Feature card — khi hover toàn bộ card, icon trong card phóng to + đổi màu nền.

<template>
  <div class="max-w-4xl mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Tính năng nổi bật</h2>

    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      <div class="group flex gap-5 p-6 bg-white rounded-xl border border-gray-200
                  hover:border-indigo-300 hover:shadow-md transition-all duration-300 cursor-pointer">
        <!-- Icon — được scale khi hover card -->
        <div class="flex-shrink-0 w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center
                    group-hover:bg-indigo-100 group-hover:scale-110 transition-all duration-300">
          <svg class="w-6 h-6 text-indigo-600 group-hover:text-indigo-700 transition-colors" 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>

        <div>
          <h3 class="text-lg font-semibold text-gray-900 group-hover:text-indigo-600 transition-colors">
            Tốc độ vượt trội
          </h3>
          <p class="mt-1 text-sm text-gray-600 leading-relaxed">
            Hệ thống được tối ưu để xử lý 10,000 request mỗi giây với độ trễ dưới 50ms.
          </p>
          <span class="inline-flex items-center gap-1 mt-3 text-sm font-medium text-indigo-600
                       opacity-0 group-hover:opacity-100 transition-opacity duration-300">
            Tìm hiểu thêm
            <svg class="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
            </svg>
          </span>
        </div>
      </div>

      <div class="group flex gap-5 p-6 bg-white rounded-xl border border-gray-200
                  hover:border-emerald-300 hover:shadow-md transition-all duration-300 cursor-pointer">
        <div class="flex-shrink-0 w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center
                    group-hover:bg-emerald-100 group-hover:scale-110 transition-all duration-300">
          <svg class="w-6 h-6 text-emerald-600 group-hover:text-emerald-700 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
          </svg>
        </div>

        <div>
          <h3 class="text-lg font-semibold text-gray-900 group-hover:text-emerald-600 transition-colors">
            Bảo mật tối đa
          </h3>
          <p class="mt-1 text-sm text-gray-600 leading-relaxed">
            Mã hóa end-to-end AES-256, SOC 2 Type II certified, GDPR compliant.
          </p>
          <span class="inline-flex items-center gap-1 mt-3 text-sm font-medium text-emerald-600
                       opacity-0 group-hover:opacity-100 transition-opacity duration-300">
            Tìm hiểu thêm
            <svg class="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
            </svg>
          </span>
        </div>
      </div>

      <div class="group flex gap-5 p-6 bg-white rounded-xl border border-gray-200
                  hover:border-purple-300 hover:shadow-md transition-all duration-300 cursor-pointer">
        <div class="flex-shrink-0 w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center
                    group-hover:bg-purple-100 group-hover:scale-110 transition-all duration-300">
          <svg class="w-6 h-6 text-purple-600 group-hover:text-purple-700 transition-colors" 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>

        <div>
          <h3 class="text-lg font-semibold text-gray-900 group-hover:text-purple-600 transition-colors">
            Tùy chỉnh linh hoạt
          </h3>
          <p class="mt-1 text-sm text-gray-600 leading-relaxed">
            API mở, webhooks, và 200+ integration sẵn có để kết nối với stack của bạn.
          </p>
          <span class="inline-flex items-center gap-1 mt-3 text-sm font-medium text-purple-600
                       opacity-0 group-hover:opacity-100 transition-opacity duration-300">
            Tìm hiểu thêm
            <svg class="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
            </svg>
          </span>
        </div>
      </div>

      <div class="group flex gap-5 p-6 bg-white rounded-xl border border-gray-200
                  hover:border-amber-300 hover:shadow-md transition-all duration-300 cursor-pointer">
        <div class="flex-shrink-0 w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center
                    group-hover:bg-amber-100 group-hover:scale-110 transition-all duration-300">
          <svg class="w-6 h-6 text-amber-600 group-hover:text-amber-700 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
          </svg>
        </div>

        <div>
          <h3 class="text-lg font-semibold text-gray-900 group-hover:text-amber-600 transition-colors">
            Hỗ trợ 24/7
          </h3>
          <p class="mt-1 text-sm text-gray-600 leading-relaxed">
            Đội ngũ support qua chat, email, và phone — thời gian phản hồi trung bình dưới 5 phút.
          </p>
          <span class="inline-flex items-center gap-1 mt-3 text-sm font-medium text-amber-600
                       opacity-0 group-hover:opacity-100 transition-opacity duration-300">
            Tìm hiểu thêm
            <svg class="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
            </svg>
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

2. Transitions (transition, transition-all, transition-colors, transition-opacity, transition-transform)

Transitions giúp thay đổi CSS diễn ra mượt mà thay vì đột ngột.

<!-- transition-all: animate mọi property thay đổi -->
<button class="bg-blue-500 hover:bg-blue-600 transition-all duration-300">
  Mượt mà mọi thứ
</button>

<!-- transition-colors: chỉ animate color (hiệu năng tốt hơn) -->
<button class="bg-blue-500 hover:bg-blue-600 text-white hover:text-blue-100 transition-colors duration-200">
  Chỉ animate màu
</button>

<!-- transition-opacity: animate opacity -->
<div class="opacity-0 hover:opacity-100 transition-opacity duration-300">
  Fade in
</div>

<!-- transition-transform: animate transform -->
<div class="hover:scale-110 transition-transform duration-300">
  Phóng to mượt
</div>

Duration, Easing, Delay

<!-- duration: kiểm soát thời gian transition -->
<div class="transition-all duration-75">75ms — gần như tức thì</div>
<div class="transition-all duration-150">150ms — nhanh, cho micro-interaction</div>
<div class="transition-all duration-300">300ms — tiêu chuẩn cho hover</div>
<div class="transition-all duration-500">500ms — chậm, cho animation lớn</div>
<div class="transition-all duration-700">700ms — rất chậm</div>
<div class="transition-all duration-1000">1000ms — 1 giây</div>

<!-- ease: kiểm soát acceleration curve -->
<div class="transition-all duration-300 ease-linear">Tốc độ đều</div>
<div class="transition-all duration-300 ease-in">Bắt đầu chậm</div>
<div class="transition-all duration-300 ease-out">Kết thúc chậm</div>
<div class="transition-all duration-300 ease-in-out">Chậm ở 2 đầu (phổ biến nhất)</div>

<!-- delay: trễ trước khi bắt đầu transition -->
<div class="hover:scale-110 transition-transform duration-300 delay-100">
  Delay 100ms trước khi phóng to
</div>
Pro tip: Luôn dùng transition cụ thể (transition-colors, transition-opacity, transition-transform) thay vì transition-all để có performance tốt hơn — browser chỉ phải tính toán property thay đổi.

transition-behavior (v4.0 NEW)

<!-- transition-behavior: kiểm soát transition khi element đang có animation khác -->
<div class="transition-all duration-300 transition-behavior-normal">
  Normal behavior
</div>
<div class="transition-all duration-300 transition-behavior-allow-discrete">
  Cho phép transition discrete properties (như display: none <-> block)
</div>

Bài tập 5: Smooth button hover color transition

Yêu cầu: Các button với hiệu ứng chuyển màu mượt, mỗi button có duration và easing khác nhau.

<template>
  <div class="max-w-2xl mx-auto p-8 space-y-6">
    <h2 class="text-2xl font-bold text-gray-900 mb-6">Button Transition Gallery</h2>

    <!-- Fast transition: 150ms -->
    <div>
      <p class="text-xs text-gray-400 mb-2">duration-150 ease-in-out (Fast — micro-interaction)</p>
      <button class="px-6 py-2.5 bg-indigo-600 text-white font-semibold rounded-lg
                     hover:bg-indigo-700 hover:shadow-md
                     transition-colors duration-150 ease-in-out">
        Hover me (150ms)
      </button>
    </div>

    <!-- Standard transition: 300ms -->
    <div>
      <p class="text-xs text-gray-400 mb-2">duration-300 ease-out (Standard — UI hover)</p>
      <button class="px-6 py-2.5 bg-emerald-600 text-white font-semibold rounded-lg
                     hover:bg-emerald-700 hover:shadow-lg hover:-translate-y-0.5
                     transition-all duration-300 ease-out">
        Hover me (300ms)
      </button>
    </div>

    <!-- Slow transition: 500ms -->
    <div>
      <p class="text-xs text-gray-400 mb-2">duration-500 ease-in-out (Slow — dramatic effect)</p>
      <button class="px-6 py-2.5 bg-purple-600 text-white font-semibold rounded-lg
                     hover:bg-purple-700 hover:rounded-2xl hover:shadow-xl
                     transition-all duration-500 ease-in-out">
        Hover me (500ms)
      </button>
    </div>

    <!-- Transition with delay -->
    <div>
      <p class="text-xs text-gray-400 mb-2">duration-300 delay-100 (có độ trễ trước khi transition)</p>
      <button class="px-6 py-2.5 border-2 border-rose-500 text-rose-500 font-semibold rounded-lg
                     hover:bg-rose-500 hover:text-white
                     transition-all duration-300 delay-100 ease-out">
        Hover me (delay 100ms)
      </button>
    </div>

    <!-- Spring-like with custom cubic-bezier concept -->
    <div>
      <p class="text-xs text-gray-400 mb-2">duration-200 + hover:scale-105 (scale effect)</p>
      <button class="px-6 py-2.5 bg-amber-500 text-white font-semibold rounded-full
                     hover:bg-amber-600 hover:scale-105
                     transition-all duration-200 ease-out
                     active:scale-95">
        Scale on hover
      </button>
    </div>
  </div>
</template>

Bài tập 6: Modal fade-in/fade-out

Yêu cầu: Modal với hiệu ứng fade-in khi mở + overlay mờ dần. Sử dụng transition-opacity.

<template>
  <div class="max-w-lg mx-auto p-8">
    <!-- Trigger button -->
    <button
      @click="isOpen = true"
      class="px-6 py-2.5 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition-colors">
      Mở Modal
    </button>

    <!-- Modal overlay + content -->
    <div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4">
      <!-- Overlay — transition opacity -->
      <div
        class="absolute inset-0 bg-black/50 transition-opacity duration-300"
        :class="showModal ? 'opacity-100' : 'opacity-0'"
        @click="closeModal">
      </div>

      <!-- Modal content — transition opacity + scale -->
      <div
        class="relative bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 transition-all duration-300"
        :class="showModal ? 'opacity-100 scale-100' : 'opacity-0 scale-95'">
        <div class="flex items-center justify-between mb-4">
          <h3 class="text-lg font-semibold text-gray-900">Xác nhận xóa</h3>
          <button
            @click="closeModal"
            class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 transition-colors">
            <span class="text-gray-400 text-xl">&times;</span>
          </button>
        </div>

        <p class="text-sm text-gray-600 leading-relaxed">
          Bạn có chắc chắn muốn xóa mục này? Hành động này không thể hoàn tác.
        </p>

        <div class="mt-6 flex gap-3 justify-end">
          <button
            @click="closeModal"
            class="px-4 py-2 border border-gray-300 text-gray-700 text-sm font-medium rounded-lg
                   hover:bg-gray-50 transition-colors">
            Hủy
          </button>
          <button
            @click="confirmDelete"
            class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg
                   hover:bg-red-700 transition-colors">
            Xóa
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'

const isOpen = ref(false)
const showModal = ref(false)

function closeModal() {
  showModal.value = false
  setTimeout(() => {
    isOpen.value = false
  }, 300) // khớp với duration-300
}

function confirmDelete() {
  // Xử lý xóa
  closeModal()
}

watch(isOpen, async (val) => {
  if (val) {
    await nextTick()
    // Trigger transition sau khi mount
    requestAnimationFrame(() => {
      showModal.value = true
    })
  }
})
</script>

Bài tập 7: Dropdown menu với slide-down transition

Yêu cầu: Dropdown menu hiện ra với hiệu ứng trượt xuống + fade in.

<template>
  <div class="max-w-sm mx-auto p-8">
    <div class="relative">
      <!-- Trigger -->
      <button
        @click="isOpen = !isOpen"
        class="w-full flex items-center justify-between px-4 py-2.5 bg-white border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
        <span>Chọn danh mục</span>
        <svg
          class="w-4 h-4 text-gray-400 transition-transform duration-200"
          :class="{ 'rotate-180': isOpen }"
          fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
        </svg>
      </button>

      <!-- Dropdown menu -->
      <div
        v-if="isOpen"
        class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden
               transition-all duration-200 ease-out origin-top"
        :class="isOpen ? 'opacity-100 scale-y-100' : 'opacity-0 scale-y-0'">
        <div class="py-1">
          <button
            v-for="item in menuItems"
            :key="item"
            class="w-full text-left px-4 py-2.5 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors">
            {{ item }}
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const isOpen = ref(false)
const menuItems = ['Frontend Development', 'Backend Development', 'DevOps', 'UI/UX Design', 'Data Science']
</script>

3. Transforms (scale-*, rotate-*, translate-x-*, translate-y-*, skew-*, transform-origin)

<!-- Scale -->
<div class="scale-50">Thu nhỏ 50%</div>
<div class="scale-75">Thu nhỏ 75%</div>
<div class="scale-90">Thu nhỏ 90%</div>
<div class="scale-100">Kích thước gốc (default)</div>
<div class="scale-105">Phóng to 5%</div>
<div class="scale-110">Phóng to 10%</div>
<div class="scale-125">Phóng to 25%</div>
<div class="scale-150">Phóng to 50%</div>

<!-- Scale riêng từng trục -->
<div class="scale-x-50 scale-y-110">Scale X 50%, Y 110%</div>

<!-- Rotate -->
<div class="rotate-0">0 deg (default)</div>
<div class="rotate-45">45 deg</div>
<div class="rotate-90">90 deg</div>
<div class="rotate-180">180 deg</div>

<!-- Translate -->
<div class="translate-x-4">Dịch phải 1rem</div>
<div class="-translate-x-4">Dịch trái 1rem</div>
<div class="translate-y-2">Dịch xuống 0.5rem</div>
<div class="-translate-y-2">Dịch lên 0.5rem</div>
<div class="translate-x-1/2">Dịch phải 50% width của element</div>

<!-- Skew -->
<div class="skew-x-3">Nghiêng X 3 deg</div>
<div class="skew-y-6">Nghiêng Y 6 deg</div>

3D Transforms (v4.0 NEW)

NEW v4.0 — Tailwind hỗ trợ đầy đủ 3D transforms: rotate-x-*, rotate-y-*, rotate-z-*, perspective-*, backface-visible.
<!-- Rotate 3D -->
<div class="rotate-x-45">Xoay quanh trục X 45 deg</div>
<div class="rotate-y-180">Xoay quanh trục Y 180 deg (lật ngang)</div>
<div class="rotate-z-90">Xoay quanh trục Z 90 deg (giống rotate-90)</div>

<!-- Perspective — cần cho container cha -->
<div class="perspective-[800px]">
  <div class="rotate-y-45">Con xoay 3D với perspective 800px</div>
</div>

<!-- backface-visibility -->
<div class="backface-visible">Mặt sau hiển thị</div>
<div class="backface-hidden">Mặt sau ẩn (cần cho card flip)</div>

Transform Origin

<div class="origin-center">Tâm (default)</div>
<div class="origin-top">Đỉnh</div>
<div class="origin-bottom-right">Góc dưới phải</div>
<div class="origin-top-left">Góc trên trái</div>

Bài tập 8: Card flip animation (3D rotate-y)

Yêu cầu: Card có 2 mặt (trước/sau), lật 180deg khi hover sử dụng 3D transform.

<template>
  <div class="max-w-sm mx-auto p-8">
    <h2 class="text-xl font-bold text-gray-900 mb-6">Card Flip 3D</h2>

    <!-- Container với perspective -->
    <div class="group perspective-[1000px]">
      <div class="relative w-full aspect-[4/3] transition-transform duration-700 ease-in-out transform-gpu group-hover:[transform:rotateY(180deg)]" style="transform-style: preserve-3d;">

        <!-- Mặt trước -->
        <div class="absolute inset-0 backface-hidden rounded-2xl overflow-hidden shadow-lg">
          <img
            src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4"
            alt="Front"
            class="w-full h-full object-cover"
          />
          <div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end p-6">
            <div>
              <h3 class="text-white text-xl font-bold">Núi Phú Sĩ</h3>
              <p class="text-white/80 text-sm mt-1">Nhật Bản</p>
            </div>
          </div>
        </div>

        <!-- Mặt sau -->
        <div class="absolute inset-0 backface-hidden rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 shadow-lg flex flex-col items-center justify-center p-6 text-center text-white" style="transform: rotateY(180deg);">
          <h3 class="text-xl font-bold">Thông tin chi tiết</h3>
          <p class="mt-3 text-white/80 text-sm leading-relaxed">
            Núi Phú Sĩ cao 3,776m — ngọn núi cao nhất Nhật Bản.
            Được UNESCO công nhận là Di sản Văn hóa Thế giới năm 2013.
          </p>
          <button class="mt-6 px-5 py-2 bg-white text-indigo-600 text-sm font-semibold rounded-lg hover:bg-indigo-50 transition-colors">
            Khám phá ngay
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

Bài tập 9: Hover scale effect on image cards

Yêu cầu: Grid ảnh — mỗi ảnh phóng to nhẹ khi hover, kèm overlay caption trượt lên.

<template>
  <div class="max-w-5xl mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Bộ sưu tập</h2>

    <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
      <!-- Card 1 -->
      <div class="group relative overflow-hidden rounded-xl aspect-[4/5] cursor-pointer">
        <img
          src="https://images.unsplash.com/photo-1506744038136-46273834b3fb"
          alt=""
          class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
        />
        <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent
                    opacity-0 group-hover:opacity-100 transition-opacity duration-300">
        </div>
        <div class="absolute bottom-0 left-0 right-0 p-5 translate-y-4 group-hover:translate-y-0
                    transition-transform duration-300">
          <h3 class="text-white font-semibold text-lg">Hồ Moraine</h3>
          <p class="text-white/70 text-sm mt-1">Banff, Canada</p>
        </div>
      </div>

      <!-- Card 2 -->
      <div class="group relative overflow-hidden rounded-xl aspect-[4/5] cursor-pointer">
        <img
          src="https://images.unsplash.com/photo-1469474968028-56623f02e42e"
          alt=""
          class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
        />
        <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent
                    opacity-0 group-hover:opacity-100 transition-opacity duration-300">
        </div>
        <div class="absolute bottom-0 left-0 right-0 p-5 translate-y-4 group-hover:translate-y-0
                    transition-transform duration-300">
          <h3 class="text-white font-semibold text-lg">Rừng nhiệt đới</h3>
          <p class="text-white/70 text-sm mt-1">Costa Rica</p>
        </div>
      </div>

      <!-- Card 3 -->
      <div class="group relative overflow-hidden rounded-xl aspect-[4/5] cursor-pointer">
        <img
          src="https://images.unsplash.com/photo-1472214103451-9374bd1c798e"
          alt=""
          class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
        />
        <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent
                    opacity-0 group-hover:opacity-100 transition-opacity duration-300">
        </div>
        <div class="absolute bottom-0 left-0 right-0 p-5 translate-y-4 group-hover:translate-y-0
                    transition-transform duration-300">
          <h3 class="text-white font-semibold text-lg">Đồng cỏ mùa thu</h3>
          <p class="text-white/70 text-sm mt-1">Tuscany, Italy</p>
        </div>
      </div>
    </div>
  </div>
</template>

Bài tập 10: Slide-in panel from right

Yêu cầu: Side panel trượt từ phải vào khi click nút, kèm overlay.

<template>
  <div class="max-w-lg mx-auto p-8">
    <button
      @click="isOpen = true"
      class="px-6 py-2.5 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition-colors">
      Mở Side Panel
    </button>

    <!-- Overlay -->
    <div
      v-if="isOpen"
      class="fixed inset-0 z-40 bg-black/30 transition-opacity duration-300"
      :class="showPanel ? 'opacity-100' : 'opacity-0'"
      @click="closePanel">
    </div>

    <!-- Side Panel -->
    <div
      v-if="isOpen"
      class="fixed top-0 right-0 z-50 h-full w-80 bg-white shadow-2xl
             transition-transform duration-300 ease-in-out"
      :class="showPanel ? 'translate-x-0' : 'translate-x-full'">
      <div class="flex items-center justify-between p-5 border-b border-gray-200">
        <h3 class="text-lg font-semibold text-gray-900">Giỏ hàng</h3>
        <button
          @click="closePanel"
          class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 transition-colors">
          <span class="text-gray-400 text-xl">&times;</span>
        </button>
      </div>

      <div class="p-5 space-y-4">
        <div class="flex gap-3">
          <div class="w-16 h-16 bg-gray-100 rounded-lg flex-shrink-0"></div>
          <div class="flex-1 min-w-0">
            <p class="text-sm font-medium text-gray-900 truncate">Sản phẩm A</p>
            <p class="text-sm text-gray-500">Số lượng: 1</p>
            <p class="text-sm font-semibold text-gray-900 mt-1">350.000đ</p>
          </div>
        </div>

        <div class="flex gap-3">
          <div class="w-16 h-16 bg-gray-100 rounded-lg flex-shrink-0"></div>
          <div class="flex-1 min-w-0">
            <p class="text-sm font-medium text-gray-900 truncate">Sản phẩm B</p>
            <p class="text-sm text-gray-500">Số lượng: 2</p>
            <p class="text-sm font-semibold text-gray-900 mt-1">590.000đ</p>
          </div>
        </div>
      </div>

      <div class="absolute bottom-0 left-0 right-0 p-5 border-t border-gray-200">
        <div class="flex justify-between mb-4">
          <span class="text-sm text-gray-600">Tổng cộng</span>
          <span class="text-lg font-bold text-gray-900">940.000đ</span>
        </div>
        <button class="w-full py-2.5 bg-indigo-600 text-white font-semibold rounded-lg
                       hover:bg-indigo-700 transition-colors">
          Thanh toán
        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'

const isOpen = ref(false)
const showPanel = ref(false)

function closePanel() {
  showPanel.value = false
  setTimeout(() => {
    isOpen.value = false
  }, 300)
}

watch(isOpen, async (val) => {
  if (val) {
    await nextTick()
    requestAnimationFrame(() => {
      showPanel.value = true
    })
  }
})
</script>

4. Animations (animate-spin, animate-ping, animate-pulse, animate-bounce)

Tailwind cung cấp sẵn 4 animation cơ bản:

<!-- Spin: xoay tròn (dùng cho loading spinner) -->
<div class="animate-spin w-8 h-8 border-4 border-blue-200 border-t-blue-600 rounded-full"></div>

<!-- Ping: lan tỏa (dùng cho notification dot) -->
<span class="relative flex h-3 w-3">
  <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
  <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>

<!-- Pulse: nhấp nháy (dùng cho skeleton loading) -->
<div class="animate-pulse bg-gray-200 rounded-lg h-4 w-3/4"></div>

<!-- Bounce: nảy lên (dùng cho scroll indicator, chat typing) -->
<div class="animate-bounce w-4 h-4 bg-indigo-500 rounded-full"></div>

Custom animations via @theme

@theme {
  /* Định nghĩa keyframes */
  --keyframes-fade-in-up: {
    from {
      opacity: 0;
      transform: translateY(20px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  };

  --keyframes-slide-in-right: {
    from {
      transform: translateX(100%);
    }
    to {
      transform: translateX(0);
    }
  };

  --keyframes-shimmer: {
    0% {
      background-position: -200% 0;
    }
    100% {
      background-position: 200% 0;
    }
  };

  /* Định nghĩa animation (shortcut) */
  --animation-fade-in-up: fade-in-up 0.5s ease-out;
  --animation-slide-in-right: slide-in-right 0.3s ease-out;
  --animation-shimmer: shimmer 1.5s infinite;
}
<div class="animate-fade-in-up">Fade in + trượt lên</div>
<div class="animate-slide-in-right">Trượt từ phải vào</div>
<div class="animate-shimmer bg-gradient-to-r from-transparent via-white/20 to-transparent bg-[length:200%_100%]">
  Shimmer loading effect
</div>

motion-safe: / motion-reduce: (Accessibility)

<!-- Chỉ chạy animation khi user không bật "prefers-reduced-motion" -->
<div class="motion-safe:animate-bounce">
  Chỉ nảy khi user không giảm chuyển động
</div>

<!-- Thay thế animation bằng static style khi user giảm chuyển động -->
<div class="animate-spin motion-reduce:animate-none motion-reduce:opacity-100">
  Spinner — nhưng thành static nếu user cần
</div>

Bài tập 11: Loading spinner component

Yêu cầu: Spinner component với nhiều kích thước và màu sắc.

<template>
  <div class="max-w-md mx-auto p-8 space-y-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-6">Loading Spinner Gallery</h2>

    <!-- Spinner nhỏ -->
    <div class="flex items-center gap-4">
      <div class="animate-spin w-4 h-4 border-2 border-gray-200 border-t-indigo-600 rounded-full"></div>
      <span class="text-sm text-gray-600">sm — 16px</span>
    </div>

    <!-- Spinner vừa -->
    <div class="flex items-center gap-4">
      <div class="animate-spin w-8 h-8 border-[3px] border-gray-200 border-t-indigo-600 rounded-full"></div>
      <span class="text-sm text-gray-600">md — 32px (mặc định)</span>
    </div>

    <!-- Spinner lớn -->
    <div class="flex items-center gap-4">
      <div class="animate-spin w-12 h-12 border-4 border-gray-200 border-t-indigo-600 rounded-full"></div>
      <span class="text-sm text-gray-600">lg — 48px</span>
    </div>

    <!-- Spinner với màu khác -->
    <div class="flex items-center gap-4">
      <div class="animate-spin w-8 h-8 border-[3px] border-emerald-200 border-t-emerald-600 rounded-full"></div>
      <span class="text-sm text-gray-600">Success spinner</span>
    </div>

    <div class="flex items-center gap-4">
      <div class="animate-spin w-8 h-8 border-[3px] border-red-200 border-t-red-600 rounded-full"></div>
      <span class="text-sm text-gray-600">Danger spinner</span>
    </div>

    <!-- Full-page loading overlay -->
    <div>
      <p class="text-sm text-gray-500 mb-3">Full-page loading overlay:</p>
      <div class="relative h-32 bg-gray-100 rounded-xl overflow-hidden">
        <div class="absolute inset-0 bg-white/80 flex items-center justify-center gap-3">
          <div class="animate-spin w-8 h-8 border-[3px] border-gray-200 border-t-indigo-600 rounded-full"></div>
          <span class="text-sm font-medium text-gray-700">Đang tải dữ liệu...</span>
        </div>
      </div>
    </div>
  </div>
</template>

Bài tập 12: Skeleton loading screen với pulse

Yêu cầu: Màn hình skeleton loading cho 1 trang blog list — các block xám nhấp nháy.

<template>
  <div class="max-w-4xl mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Skeleton Loading — Blog List</h2>

    <div class="space-y-8">
      <!-- Skeleton card 1 -->
      <div class="flex gap-6 p-4 bg-white rounded-xl border border-gray-100">
        <!-- Thumbnail skeleton -->
        <div class="animate-pulse bg-gray-200 rounded-lg w-48 h-32 flex-shrink-0"></div>

        <!-- Content skeleton -->
        <div class="flex-1 space-y-3">
          <!-- Category tag -->
          <div class="animate-pulse bg-gray-200 rounded-full w-20 h-5"></div>

          <!-- Title (2 dòng) -->
          <div class="animate-pulse bg-gray-200 rounded h-5 w-3/4"></div>
          <div class="animate-pulse bg-gray-200 rounded h-5 w-1/2"></div>

          <!-- Description (3 dòng) -->
          <div class="animate-pulse bg-gray-100 rounded h-3 w-full"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-5/6"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-2/3"></div>

          <!-- Author + date -->
          <div class="flex gap-4">
            <div class="animate-pulse bg-gray-200 rounded-full w-8 h-8"></div>
            <div class="animate-pulse bg-gray-200 rounded h-3 w-24"></div>
            <div class="animate-pulse bg-gray-200 rounded h-3 w-16"></div>
          </div>
        </div>
      </div>

      <!-- Skeleton card 2 -->
      <div class="flex gap-6 p-4 bg-white rounded-xl border border-gray-100">
        <div class="animate-pulse bg-gray-200 rounded-lg w-48 h-32 flex-shrink-0"></div>
        <div class="flex-1 space-y-3">
          <div class="animate-pulse bg-gray-200 rounded-full w-16 h-5"></div>
          <div class="animate-pulse bg-gray-200 rounded h-5 w-2/3"></div>
          <div class="animate-pulse bg-gray-200 rounded h-5 w-1/3"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-full"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-4/5"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-1/2"></div>
          <div class="flex gap-4">
            <div class="animate-pulse bg-gray-200 rounded-full w-8 h-8"></div>
            <div class="animate-pulse bg-gray-200 rounded h-3 w-24"></div>
          </div>
        </div>
      </div>

      <!-- Skeleton card 3 -->
      <div class="flex gap-6 p-4 bg-white rounded-xl border border-gray-100">
        <div class="animate-pulse bg-gray-200 rounded-lg w-48 h-32 flex-shrink-0"></div>
        <div class="flex-1 space-y-3">
          <div class="animate-pulse bg-gray-200 rounded-full w-24 h-5"></div>
          <div class="animate-pulse bg-gray-200 rounded h-5 w-4/5"></div>
          <div class="animate-pulse bg-gray-200 rounded h-5 w-1/2"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-full"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-3/4"></div>
          <div class="animate-pulse bg-gray-100 rounded h-3 w-1/3"></div>
          <div class="flex gap-4">
            <div class="animate-pulse bg-gray-200 rounded-full w-8 h-8"></div>
            <div class="animate-pulse bg-gray-200 rounded h-3 w-20"></div>
            <div class="animate-pulse bg-gray-200 rounded h-3 w-14"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

Bài tập 13: Fade-in-up animation cho page sections (scroll reveal concept)

Yêu cầu: Các section xuất hiện với hiệu ứng fade-in + trượt lên khi scroll (dùng CSS animation).

<template>
  <div class="max-w-3xl mx-auto p-8 space-y-16">
    <!-- Section 1 -->
    <section class="animate-fade-in-up">
      <span class="text-sm font-semibold text-indigo-600 uppercase tracking-wider">Section 1</span>
      <h2 class="text-3xl font-bold text-gray-900 mt-2">Tại sao chọn chúng tôi?</h2>
      <p class="mt-4 text-gray-600 leading-relaxed">
        Với hơn 10 năm kinh nghiệm trong ngành, chúng tôi đã giúp hơn 500 doanh nghiệp
        chuyển đổi số thành công. Đội ngũ 200+ kỹ sư luôn sẵn sàng đồng hành cùng bạn.
      </p>
    </section>

    <!-- Section 2 — delay -->
    <section class="animate-fade-in-up" style="animation-delay: 0.2s; animation-fill-mode: both;">
      <span class="text-sm font-semibold text-emerald-600 uppercase tracking-wider">Section 2</span>
      <h2 class="text-3xl font-bold text-gray-900 mt-2">Công nghệ hiện đại</h2>
      <p class="mt-4 text-gray-600 leading-relaxed">
        Sử dụng stack công nghệ mới nhất: Nuxt 4, TypeScript, PostgreSQL,
        Docker, Kubernetes — đảm bảo sản phẩm luôn ở trạng thái tốt nhất.
      </p>
    </section>

    <!-- Section 3 — delay thêm -->
    <section class="animate-fade-in-up" style="animation-delay: 0.4s; animation-fill-mode: both;">
      <span class="text-sm font-semibold text-purple-600 uppercase tracking-wider">Section 3</span>
      <h2 class="text-3xl font-bold text-gray-900 mt-2">Bảo mật và tin cậy</h2>
      <p class="mt-4 text-gray-600 leading-relaxed">
        SOC 2 Type II certified, GDPR compliant. Dữ liệu của bạn được mã hóa
        end-to-end và lưu trữ trên hạ tầng đạt chuẩn quốc tế.
      </p>
    </section>
  </div>
</template>

<style scoped>
@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-fade-in-up {
  animation: fade-in-up 0.6s ease-out both;
}
</style>

5. Opacity & Visibility

<!-- Opacity -->
<div class="opacity-100">Hiển thị đầy đủ</div>
<div class="opacity-75">75% opacity</div>
<div class="opacity-50">50% opacity</div>
<div class="opacity-25">25% opacity</div>
<div class="opacity-0">Ẩn hoàn toàn (vẫn chiếm không gian)</div>

<!-- Visibility -->
<div class="visible">Hiển thị (default)</div>
<div class="invisible">Ẩn nhưng vẫn chiếm không gian</div>
<div class="collapse">Ẩn + không chiếm không gian (nhưng khác display:none)</div>

Bài tập 14: Tooltip show/hide on hover

Yêu cầu: Tooltip xuất hiện khi hover vào icon với transition opacity + translate.

<template>
  <div class="max-w-md mx-auto p-8 pt-16">
    <h2 class="text-xl font-bold text-gray-900 mb-8">Tooltip trên hover</h2>

    <div class="flex gap-8 justify-center">
      <!-- Tooltip top -->
      <div class="relative group">
        <button class="w-10 h-10 bg-indigo-100 text-indigo-600 rounded-lg flex items-center justify-center
                       hover:bg-indigo-200 transition-colors text-lg font-bold">
          ?
        </button>
        <div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-gray-900 text-white text-xs rounded-lg
                    opacity-0 invisible group-hover:opacity-100 group-hover:visible
                    transition-all duration-200 whitespace-nowrap">
          Tooltip phía trên
          <!-- Arrow -->
          <div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
        </div>
      </div>

      <!-- Tooltip bottom -->
      <div class="relative group">
        <button class="w-10 h-10 bg-emerald-100 text-emerald-600 rounded-lg flex items-center justify-center
                       hover:bg-emerald-200 transition-colors text-lg">
          &#9432;
        </button>
        <div class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 bg-gray-900 text-white text-xs rounded-lg
                    opacity-0 invisible group-hover:opacity-100 group-hover:visible
                    transition-all duration-200 whitespace-nowrap">
          Thông tin chi tiết
          <div class="absolute bottom-full left-1/2 -translate-x-1/2 border-4 border-transparent border-b-gray-900"></div>
        </div>
      </div>

      <!-- Tooltip left -->
      <div class="relative group">
        <button class="w-10 h-10 bg-amber-100 text-amber-600 rounded-lg flex items-center justify-center
                       hover:bg-amber-200 transition-colors text-lg">
          &#9888;
        </button>
        <div class="absolute right-full top-1/2 -translate-y-1/2 mr-2 px-3 py-1.5 bg-gray-900 text-white text-xs rounded-lg
                    opacity-0 invisible group-hover:opacity-100 group-hover:visible
                    transition-all duration-200 whitespace-nowrap">
          Cảnh báo
          <div class="absolute left-full top-1/2 -translate-y-1/2 border-4 border-transparent border-l-gray-900"></div>
        </div>
      </div>

      <!-- Tooltip right -->
      <div class="relative group">
        <button class="w-10 h-10 bg-rose-100 text-rose-600 rounded-lg flex items-center justify-center
                       hover:bg-rose-200 transition-colors text-lg font-bold">
          !
        </button>
        <div class="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-3 py-1.5 bg-gray-900 text-white text-xs rounded-lg
                    opacity-0 invisible group-hover:opacity-100 group-hover:visible
                    transition-all duration-200 whitespace-nowrap">
          Lỗi hệ thống
          <div class="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900"></div>
        </div>
      </div>
    </div>
  </div>
</template>

6. Filters (blur-*, brightness-*, contrast-*, grayscale-*, hue-rotate-*, invert-*, saturate-*, sepia-*, drop-shadow-*)

CSS filters cho phép áp dụng hiệu ứng đồ họa lên element — cực kỳ hữu ích cho hiệu ứng hover ảnh.

<!-- Blur -->
<div class="blur-sm">Làm mờ nhẹ</div>
<div class="blur">Làm mờ vừa</div>
<div class="blur-md">Làm mờ trung bình</div>
<div class="blur-lg">Làm mờ nhiều</div>
<div class="blur-xl">Làm mờ rất nhiều</div>
<div class="blur-2xl">Làm mờ tối đa</div>
<div class="blur-3xl">Làm mờ cực đại</div>

<!-- Brightness -->
<div class="brightness-50">Tối 50%</div>
<div class="brightness-75">Tối 25%</div>
<div class="brightness-100">Bình thường</div>
<div class="brightness-125">Sáng 25%</div>
<div class="brightness-150">Sáng 50%</div>
<div class="brightness-200">Sáng gấp đôi</div>

<!-- Contrast -->
<div class="contrast-50">Tương phản thấp</div>
<div class="contrast-100">Bình thường</div>
<div class="contrast-150">Tương phản cao</div>
<div class="contrast-200">Tương phản rất cao</div>

<!-- Grayscale -->
<div class="grayscale-0">Không grayscale (default)</div>
<div class="grayscale">Grayscale 100%</div>

<!-- Các filter khác -->
<div class="hue-rotate-60">Xoay hue 60 deg</div>
<div class="invert">Đảo màu</div>
<div class="saturate-50">Giảm bão hòa</div>
<div class="saturate-150">Tăng bão hòa</div>
<div class="sepia">Hiệu ứng ảnh cũ</div>

Backdrop Filters

Backdrop filters áp dụng hiệu ứng lên vùng phía sau element — thường dùng cho glassmorphism.

<!-- Backdrop blur (glass effect) -->
<div class="backdrop-blur-sm bg-white/30">Glass nhẹ</div>
<div class="backdrop-blur-md bg-white/40">Glass vừa</div>
<div class="backdrop-blur-lg bg-white/50">Glass mạnh</div>
<div class="backdrop-blur-xl bg-white/60">Glass rất mạnh</div>

<!-- Kết hợp backdrop filters -->
<div class="backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-white/30">
  Glassmorphism đầy đủ
</div>

Bài tập 15: Image hover effects (grayscale -> color, blur overlay)

Yêu cầu: Grid ảnh với các hiệu ứng filter khác nhau khi hover.

<template>
  <div class="max-w-5xl mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Image Filter Effects on Hover</h2>

    <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
      <!-- Effect 1: Grayscale -> Color -->
      <div class="overflow-hidden rounded-xl">
        <img
          src="https://images.unsplash.com/photo-1542291026-7eec264c27ff"
          alt=""
          class="w-full aspect-square object-cover grayscale hover:grayscale-0 transition-all duration-500"
        />
        <p class="mt-2 text-sm text-center text-gray-500">Grayscale &rarr; Color</p>
      </div>

      <!-- Effect 2: Blur -> Sharp -->
      <div class="overflow-hidden rounded-xl">
        <img
          src="https://images.unsplash.com/photo-1560769629-975ec94e6a86"
          alt=""
          class="w-full aspect-square object-cover blur-sm hover:blur-0 transition-all duration-500"
        />
        <p class="mt-2 text-sm text-center text-gray-500">Blur &rarr; Sharp</p>
      </div>

      <!-- Effect 3: Brightness -->
      <div class="overflow-hidden rounded-xl">
        <img
          src="https://images.unsplash.com/photo-1572635196237-14b3f281503f"
          alt=""
          class="w-full aspect-square object-cover brightness-75 hover:brightness-110 transition-all duration-500"
        />
        <p class="mt-2 text-sm text-center text-gray-500">Dark &rarr; Bright</p>
      </div>

      <!-- Effect 4: Saturate -->
      <div class="overflow-hidden rounded-xl">
        <img
          src="https://images.unsplash.com/photo-1585386959984-a4155224a1ad"
          alt=""
          class="w-full aspect-square object-cover saturate-0 hover:saturate-150 transition-all duration-500"
        />
        <p class="mt-2 text-sm text-center text-gray-500">B&W &rarr; Saturated</p>
      </div>

      <!-- Effect 5: Sepia -->
      <div class="overflow-hidden rounded-xl">
        <img
          src="https://images.unsplash.com/photo-1491553895911-0055eca6402d"
          alt=""
          class="w-full aspect-square object-cover sepia-0 hover:sepia transition-all duration-500"
        />
        <p class="mt-2 text-sm text-center text-gray-500">Normal &rarr; Sepia</p>
      </div>

      <!-- Effect 6: Hue rotate -->
      <div class="overflow-hidden rounded-xl">
        <img
          src="https://images.unsplash.com/photo-1526170375885-4d8ecf77b99f"
          alt=""
          class="w-full aspect-square object-cover hover:hue-rotate-180 transition-all duration-700"
        />
        <p class="mt-2 text-sm text-center text-gray-500">Hue Rotate 180 deg</p>
      </div>
    </div>
  </div>
</template>

Bài tập 16: Glassmorphism card với backdrop-blur + bg-opacity

Yêu cầu: Card với hiệu ứng kính mờ (glassmorphism) trên nền gradient/ảnh.

<template>
  <div class="relative max-w-2xl mx-auto min-h-[500px] rounded-2xl overflow-hidden">
    <!-- Background -->
    <div class="absolute inset-0 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500">
      <!-- Decorative circles -->
      <div class="absolute -top-20 -left-20 w-64 h-64 bg-white/10 rounded-full"></div>
      <div class="absolute -bottom-20 -right-20 w-80 h-80 bg-white/10 rounded-full"></div>
      <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-48 h-48 bg-white/5 rounded-full"></div>
    </div>

    <!-- Glass card -->
    <div class="relative z-10 max-w-sm mx-auto mt-20 p-8 rounded-2xl
                bg-white/20 backdrop-blur-xl backdrop-saturate-150
                border border-white/30 shadow-2xl">
      <div class="w-16 h-16 bg-white/30 rounded-2xl flex items-center justify-center mb-6
                  backdrop-blur-sm">
        <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
        </svg>
      </div>

      <h2 class="text-2xl font-bold text-white">Glassmorphism Card</h2>
      <p class="mt-3 text-white/80 text-sm leading-relaxed">
        Hiệu ứng kính mờ (frosted glass) được tạo bằng cách kết hợp
        backdrop-blur, background opacity, và border trong suốt.
      </p>

      <div class="mt-6 space-y-3">
        <div class="flex items-center gap-3">
          <div class="w-10 h-10 bg-white/10 rounded-lg backdrop-blur-sm flex items-center justify-center text-white text-sm font-bold">1</div>
          <span class="text-white/90 text-sm">backdrop-blur-xl</span>
        </div>
        <div class="flex items-center gap-3">
          <div class="w-10 h-10 bg-white/10 rounded-lg backdrop-blur-sm flex items-center justify-center text-white text-sm font-bold">2</div>
          <span class="text-white/90 text-sm">bg-white/20</span>
        </div>
        <div class="flex items-center gap-3">
          <div class="w-10 h-10 bg-white/10 rounded-lg backdrop-blur-sm flex items-center justify-center text-white text-sm font-bold">3</div>
          <span class="text-white/90 text-sm">border-white/30</span>
        </div>
      </div>

      <button class="mt-8 w-full py-2.5 bg-white/25 backdrop-blur-sm text-white font-semibold rounded-xl
                     border border-white/30 hover:bg-white/35 transition-colors">
        Khám phá ngay
      </button>
    </div>
  </div>
</template>

Bài tập 17: Navbar với backdrop-blur (frosted glass effect)

Yêu cầu: Thanh navbar cố định với hiệu ứng kính mờ khi scroll.

<template>
  <div class="max-w-4xl mx-auto">
    <!-- Navbar -->
    <nav class="sticky top-0 z-50 bg-white/70 backdrop-blur-lg backdrop-saturate-150 border-b border-gray-200/50">
      <div class="max-w-5xl mx-auto px-4 sm:px-6">
        <div class="flex items-center justify-between h-16">
          <!-- Logo -->
          <a href="#" class="text-xl font-black text-gray-900 tracking-tight">
            Brand<span class="text-indigo-600">.</span>
          </a>

          <!-- Nav links -->
          <div class="hidden md:flex items-center gap-1">
            <a href="#" class="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 rounded-lg hover:bg-gray-100/80 transition-colors">Home</a>
            <a href="#" class="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 rounded-lg hover:bg-gray-100/80 transition-colors">Features</a>
            <a href="#" class="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 rounded-lg hover:bg-gray-100/80 transition-colors">Pricing</a>
            <a href="#" class="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 rounded-lg hover:bg-gray-100/80 transition-colors">Blog</a>
          </div>

          <!-- Right actions -->
          <div class="flex items-center gap-3">
            <button class="hidden sm:block px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors">
              Sign in
            </button>
            <button class="px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg
                           hover:bg-gray-800 transition-colors">
              Get started
            </button>
          </div>
        </div>
      </div>
    </nav>

    <!-- Content để demo scroll -->
    <div class="px-6 py-12 space-y-8">
      <div v-for="i in 10" :key="i" class="p-6 bg-white rounded-xl border border-gray-200">
        <h3 class="text-lg font-semibold text-gray-900">Section {{ i }}</h3>
        <p class="mt-2 text-gray-600">Nội dung demo để thấy hiệu ứng backdrop-blur của navbar khi scroll qua.</p>
      </div>
    </div>
  </div>
</template>

7. Cursor & Pointer Events

<!-- Cursor styles -->
<div class="cursor-auto">Browser tự chọn (default)</div>
<div class="cursor-default">Con trỏ mặc định</div>
<div class="cursor-pointer">Bàn tay (link, button)</div>
<div class="cursor-wait">Đồng hồ cát — loading</div>
<div class="cursor-text">Con trỏ văn bản</div>
<div class="cursor-move">Con trỏ di chuyển</div>
<div class="cursor-not-allowed">Cấm — disabled state</div>
<div class="cursor-zoom-in">Kính lúp +</div>
<div class="cursor-grab">Bàn tay nắm</div>
<div class="cursor-grabbing">Bàn tay đang nắm</div>
<div class="cursor-help">Dấu hỏi</div>
<div class="cursor-crosshair">Dấu thập</div>
<div class="cursor-none">Ẩn con trỏ</div>

<!-- Pointer events -->
<div class="pointer-events-none">Không nhận click/hover (click xuyên qua)</div>
<div class="pointer-events-auto">Nhận click/hover bình thường</div>

Bài tập 18: Custom cursor states cho các interactive element khác nhau

Yêu cầu: Component demo tất cả các cursor states với context sử dụng thực tế.

<template>
  <div class="max-w-2xl mx-auto p-8 space-y-6">
    <h2 class="text-2xl font-bold text-gray-900 mb-6">Cursor States Reference</h2>

    <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
      <!-- cursor-pointer -->
      <div class="cursor-pointer p-4 bg-indigo-50 border border-indigo-200 rounded-lg hover:bg-indigo-100 transition-colors">
        <p class="text-sm font-medium text-indigo-900">cursor-pointer</p>
        <p class="text-xs text-indigo-600 mt-1">Link, button, element clickable</p>
      </div>

      <!-- cursor-not-allowed -->
      <div class="cursor-not-allowed p-4 bg-gray-50 border border-gray-200 rounded-lg">
        <p class="text-sm font-medium text-gray-500">cursor-not-allowed</p>
        <p class="text-xs text-gray-400 mt-1">Disabled button / input</p>
      </div>

      <!-- cursor-wait -->
      <div class="cursor-wait p-4 bg-amber-50 border border-amber-200 rounded-lg">
        <p class="text-sm font-medium text-amber-900">cursor-wait</p>
        <p class="text-xs text-amber-600 mt-1">Đang xử lý, chờ đợi</p>
      </div>

      <!-- cursor-text -->
      <div class="cursor-text p-4 bg-emerald-50 border border-emerald-200 rounded-lg">
        <p class="text-sm font-medium text-emerald-900">cursor-text</p>
        <p class="text-xs text-emerald-600 mt-1">Vùng text có thể chọn / input</p>
      </div>

      <!-- cursor-move -->
      <div class="cursor-move p-4 bg-purple-50 border border-purple-200 rounded-lg">
        <p class="text-sm font-medium text-purple-900">cursor-move</p>
        <p class="text-xs text-purple-600 mt-1">Element có thể kéo thả</p>
      </div>

      <!-- cursor-grab -->
      <div class="cursor-grab p-4 bg-rose-50 border border-rose-200 rounded-lg active:cursor-grabbing">
        <p class="text-sm font-medium text-rose-900">cursor-grab (click để thấy grabbing)</p>
        <p class="text-xs text-rose-600 mt-1">Drag handle, slider thumb</p>
      </div>

      <!-- cursor-zoom-in -->
      <div class="cursor-zoom-in p-4 bg-cyan-50 border border-cyan-200 rounded-lg">
        <p class="text-sm font-medium text-cyan-900">cursor-zoom-in</p>
        <p class="text-xs text-cyan-600 mt-1">Ảnh có thể phóng to</p>
      </div>

      <!-- cursor-help -->
      <div class="cursor-help p-4 bg-teal-50 border border-teal-200 rounded-lg">
        <p class="text-sm font-medium text-teal-900">cursor-help</p>
        <p class="text-xs text-teal-600 mt-1">Icon tooltip, trợ giúp</p>
      </div>
    </div>

    <!-- pointer-events demo -->
    <div class="mt-8 p-6 bg-gray-900 rounded-xl text-white">
      <h3 class="text-lg font-semibold mb-3">pointer-events Demo</h3>
      <div class="relative p-4 bg-gray-800 rounded-lg">
        <p class="text-sm text-gray-300">
          Text này nằm phía dưới overlay trong suốt
        </p>
        <!-- Overlay không chặn click -->
        <div class="absolute inset-0 bg-indigo-500/20 pointer-events-none rounded-lg flex items-center justify-center">
          <span class="text-xs text-indigo-300 font-medium">Overlay với pointer-events-none — click xuyên qua được</span>
        </div>
      </div>
    </div>
  </div>
</template>

8. COMPREHENSIVE EXERCISE — Interactive Components

Tổng hợp: Các bài tập dưới đây kết hợp nhiều kiến thức đã học — states, transitions, transforms, animations — để xây dựng component tương tác hoàn chỉnh thường gặp trong dự án thực tế.

Bài tập 19: Accordion (collapsible FAQ section) — transition + rotate

Yêu cầu: FAQ accordion với:

  • Click để mở/đóng
  • Nội dung trượt xuống mượt (max-height transition)
  • Icon mũi tên xoay 180deg khi mở
  • Chỉ 1 item mở tại 1 thời điểm
<template>
  <div class="max-w-2xl mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Câu hỏi thường gặp</h2>

    <div class="space-y-3">
      <div
        v-for="(faq, index) in faqs"
        :key="index"
        class="bg-white rounded-xl border border-gray-200 overflow-hidden
               transition-shadow duration-300"
        :class="{ 'shadow-md border-gray-300': activeIndex === index }">
        <!-- Header -->
        <button
          @click="toggle(index)"
          class="w-full flex items-center justify-between px-6 py-4 text-left
                 hover:bg-gray-50 transition-colors duration-200">
          <span class="text-base font-semibold text-gray-900 pr-4">
            {{ faq.question }}
          </span>
          <!-- Icon xoay -->
          <svg
            class="w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-300"
            :class="{ 'rotate-180': activeIndex === index }"
            fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
          </svg>
        </button>

        <!-- Content (collapsible) -->
        <div
          class="transition-all duration-300 ease-in-out overflow-hidden"
          :style="{ maxHeight: activeIndex === index ? $refs[`content-${index}`]?.scrollHeight + 'px' : '0px' }">
          <div :ref="`content-${index}`" class="px-6 pb-5">
            <p class="text-sm text-gray-600 leading-relaxed">
              {{ faq.answer }}
            </p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const activeIndex = ref<number | null>(null)

const faqs = [
  {
    question: 'Tailwind CSS có phù hợp cho dự án lớn không?',
    answer: 'Có. Tailwind rất phù hợp cho dự án lớn nhờ tính nhất quán của design system. Bạn có thể extract component (Vue/React) để tái sử dụng, dùng @apply cho pattern lặp lại, và bundle size chỉ giữ những class bạn thực sự dùng. Nhiều công ty lớn như GitHub, Shopify, NASA đều dùng Tailwind.'
  },
  {
    question: 'Làm sao để custom theme trong Tailwind v4?',
    answer: 'Trong Tailwind v4, bạn dùng CSS-based configuration với @theme directive trong file CSS chính. Không cần tailwind.config.js nữa. Định nghĩa custom colors, fonts, spacing, shadows... trực tiếp trong @theme block, tận dụng CSS cascade và CSS variables. Cách này type-safe hơn và dễ maintain hơn so với config JS.'
  },
  {
    question: 'Tailwind có hỗ trợ dark mode không?',
    answer: 'Có, Tailwind hỗ trợ dark mode qua variant dark:. Mặc định dùng media query prefers-color-scheme, hoặc bạn có thể config thành class-based (thêm class="dark" vào html element). Tất cả utility đều có thể dùng với dark: prefix — vd: dark:bg-gray-900 dark:text-white.'
  },
  {
    question: 'Khác biệt chính giữa Tailwind v3 và v4 là gì?',
    answer: 'Tailwind v4 chuyển sang CSS-based configuration (bỏ tailwind.config.js), dùng OKLCH color space (perceptually uniform, P3 wide gamut), build nhanh hơn nhờ Rust-based engine (Oxide), hỗ trợ 3D transforms, radial/conic gradients, inset-shadow/inset-ring, user-valid/user-invalid states, và nhiều cải tiến về variant như not-*, inert:.'
  }
]

function toggle(index: number) {
  activeIndex.value = activeIndex.value === index ? null : index
}
</script>

Bài tập 20: Tabs component — active state + content switching

Yêu cầu: Tabs component với:

  • Tab active có style khác biệt (bg-white, shadow, border)
  • Indicator trượt khi chuyển tab
  • Nội dung chuyển đổi với fade transition
<template>
  <div class="max-w-2xl mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Tabs Component</h2>

    <!-- Tab buttons -->
    <div class="relative flex bg-gray-100 rounded-xl p-1">
      <!-- Sliding indicator -->
      <div
        class="absolute top-1 bottom-1 bg-white rounded-lg shadow-sm border border-gray-200 transition-all duration-300 ease-in-out"
        :style="indicatorStyle">
      </div>

      <button
        v-for="(tab, index) in tabs"
        :key="tab.id"
        @click="activeTab = tab.id"
        class="relative z-10 flex-1 py-2.5 text-sm font-medium rounded-lg transition-colors duration-200"
        :class="activeTab === tab.id ? 'text-gray-900' : 'text-gray-500 hover:text-gray-700'">
        {{ tab.label }}
      </button>
    </div>

    <!-- Tab content -->
    <div class="mt-6 relative">
      <div
        v-for="tab in tabs"
        :key="tab.id"
        class="transition-all duration-300"
        :class="activeTab === tab.id
          ? 'opacity-100 translate-y-0 relative'
          : 'opacity-0 translate-y-2 absolute inset-0 pointer-events-none'">
        <div class="p-6 bg-white rounded-xl border border-gray-200 shadow-sm">
          <h3 class="text-lg font-semibold text-gray-900">{{ tab.title }}</h3>
          <p class="mt-3 text-sm text-gray-600 leading-relaxed">{{ tab.content }}</p>

          <div v-if="tab.stats" class="mt-6 grid grid-cols-3 gap-4">
            <div v-for="stat in tab.stats" :key="stat.label" class="text-center p-3 bg-gray-50 rounded-lg">
              <p class="text-2xl font-bold text-gray-900">{{ stat.value }}</p>
              <p class="text-xs text-gray-500 mt-1">{{ stat.label }}</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

const activeTab = ref('overview')

const tabs = [
  {
    id: 'overview',
    label: 'Tổng quan',
    title: 'Tổng quan dự án',
    content: 'Dự án được xây dựng với Nuxt 4 và Tailwind CSS v4, sử dụng kiến trúc hybrid rendering để tối ưu cả SEO và performance. Backend API được xây dựng trên Nitro server với PostgreSQL làm database chính.',
    stats: [
      { label: 'Thành viên', value: '12' },
      { label: 'Sprint', value: '8' },
      { label: 'Commits', value: '1.2k' }
    ]
  },
  {
    id: 'features',
    label: 'Tính năng',
    title: 'Tính năng chính',
    content: 'Hệ thống bao gồm authentication với JWT, real-time notifications qua WebSocket, file upload với Cloudflare R2, full-text search với PostgreSQL, và admin dashboard để quản lý nội dung.',
    stats: [
      { label: 'Modules', value: '24' },
      { label: 'API Routes', value: '56' },
      { label: 'Components', value: '89' }
    ]
  },
  {
    id: 'tech',
    label: 'Công nghệ',
    title: 'Stack công nghệ',
    content: 'Frontend: Nuxt 4 + Vue 3 Composition API + Tailwind CSS v4 + TypeScript. Backend: Nitro server + PostgreSQL + Redis. DevOps: Docker + GitHub Actions + Cloudflare Pages. Monitoring: Sentry + Grafana.',
    stats: [
      { label: 'Languages', value: '3' },
      { label: 'Services', value: '7' },
      { label: 'Uptime', value: '99.9%' }
    ]
  }
]

const indicatorStyle = computed(() => {
  const index = tabs.findIndex(t => t.id === activeTab.value)
  const width = 100 / tabs.length
  return {
    width: `${width}%`,
    left: `${index * width}%`
  }
})
</script>

Bài tập 21: Toast notification — slide-in + auto-dismiss animation

Yêu cầu: Toast notification component với:

  • Trượt từ phải vào (slide-in-right)
  • Auto dismiss sau 4 giây với fade-out
  • Nhiều loại: success, error, warning, info
  • Progress bar đếm ngược thời gian
<template>
  <div class="max-w-lg mx-auto p-8">
    <h2 class="text-2xl font-bold text-gray-900 mb-8">Toast Notification System</h2>

    <!-- Trigger buttons -->
    <div class="flex flex-wrap gap-3">
      <button @click="addToast('success')"
        class="px-4 py-2 bg-emerald-600 text-white text-sm font-medium rounded-lg
               hover:bg-emerald-700 transition-colors">
        Success Toast
      </button>
      <button @click="addToast('error')"
        class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg
               hover:bg-red-700 transition-colors">
        Error Toast
      </button>
      <button @click="addToast('warning')"
        class="px-4 py-2 bg-amber-500 text-white text-sm font-medium rounded-lg
               hover:bg-amber-600 transition-colors">
        Warning Toast
      </button>
      <button @click="addToast('info')"
        class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg
               hover:bg-blue-700 transition-colors">
        Info Toast
      </button>
    </div>

    <!-- Toast container -->
    <div class="fixed top-4 right-4 z-50 space-y-3 w-80">
      <div
        v-for="toast in toasts"
        :key="toast.id"
        class="relative overflow-hidden bg-white rounded-xl border border-gray-200 shadow-lg p-4
               transition-all duration-300"
        :class="toast.isLeaving
          ? 'opacity-0 translate-x-full'
          : 'opacity-100 translate-x-0'">

        <div class="flex items-start gap-3">
          <!-- Icon -->
          <div class="flex-shrink-0 w-9 h-9 rounded-lg flex items-center justify-center"
               :class="iconBgClass(toast.type)">
            <span class="text-lg">{{ iconMap[toast.type] }}</span>
          </div>

          <!-- Content -->
          <div class="flex-1 min-w-0">
            <p class="text-sm font-semibold text-gray-900">
              {{ titleMap[toast.type] }}
            </p>
            <p class="text-sm text-gray-600 mt-0.5">
              {{ messageMap[toast.type] }}
            </p>
          </div>

          <!-- Close button -->
          <button
            @click="removeToast(toast.id)"
            class="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-md
                   hover:bg-gray-100 transition-colors">
            <span class="text-gray-400 text-sm">&times;</span>
          </button>
        </div>

        <!-- Progress bar -->
        <div class="absolute bottom-0 left-0 right-0 h-1 bg-gray-100">
          <div
            class="h-full transition-all duration-[4000ms] ease-linear"
            :class="progressBarClass(toast.type)"
            :style="{ width: toast.isLeaving ? '100%' : '0%' }">
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Toast {
  id: number
  type: 'success' | 'error' | 'warning' | 'info'
  isLeaving: boolean
}

const toasts = ref<Toast[]>([])
let nextId = 0

const iconMap: Record<string, string> = {
  success: '',
  error: '',
  warning: '⚠️',
  info: 'ℹ️'
}

const titleMap: Record<string, string> = {
  success: 'Thanh cong',
  error: 'Loi',
  warning: 'Canh bao',
  info: 'Thong tin'
}

const messageMap: Record<string, string> = {
  success: 'Thao tac da duoc thuc hien thanh cong.',
  error: 'Da xay ra loi. Vui long thu lai sau.',
  warning: 'He thong sap bao tri vao luc 2:00 AM.',
  info: 'Phien ban moi v2.4.0 da san sang.'
}

function iconBgClass(type: string): string {
  const map: Record<string, string> = {
    success: 'bg-emerald-100',
    error: 'bg-red-100',
    warning: 'bg-amber-100',
    info: 'bg-blue-100'
  }
  return map[type] || 'bg-gray-100'
}

function progressBarClass(type: string): string {
  const map: Record<string, string> = {
    success: 'bg-emerald-500',
    error: 'bg-red-500',
    warning: 'bg-amber-500',
    info: 'bg-blue-500'
  }
  return map[type] || 'bg-gray-500'
}

function addToast(type: 'success' | 'error' | 'warning' | 'info') {
  const id = nextId++
  const toast: Toast = { id, type, isLeaving: false }
  toasts.value.push(toast)

  // Auto dismiss sau 4 giây
  setTimeout(() => {
    const t = toasts.value.find(t => t.id === id)
    if (t) t.isLeaving = true

    // Xóa khỏi array sau khi transition kết thúc
    setTimeout(() => {
      toasts.value = toasts.value.filter(t => t.id !== id)
    }, 300)
  }, 4000)
}

function removeToast(id: number) {
  const t = toasts.value.find(t => t.id === id)
  if (t) t.isLeaving = true

  setTimeout(() => {
    toasts.value = toasts.value.filter(t => t.id !== id)
  }, 300)
}
</script>

Tiếp theo: Tiếp tục học về Layout (Flexbox, Grid, Spacing, Sizing) và Responsive Design trong phần tiếp theo.

© 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.