Interactive States, Transitions & Animations
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" />
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">★</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: và 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> và
<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>
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">×</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)
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">×</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">
ⓘ
</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">
⚠
</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 → 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 → 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 → 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 → 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 → 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
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">×</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>
2. Typography & Colors
Tailwind CSS v4 — font, text, color (OKLCH), background, gradient, border, outline. Bài tập thiết kế typography system và color palette.
4. Responsive & Dark Mode
Tailwind CSS v4 — responsive breakpoints, container queries, dark mode, custom variants. Bài tập responsive layout và dark mode theme.