Nếu Transition là hiệu ứng chuyển tiếp giữa hai trạng thái, thì Animation là hoạt ảnh phức tạp với nhiều bước. Animation cho phép bạn tạo những chuyển động lặp lại, phức tạp hơn nhiều — từ loading spinner cho đến parallax effect.
Bài này sẽ dạy bạn cách sử dụng @keyframes và animation để tạo hoạt ảnh CSS tuyệt vời mà không cần JavaScript.
Animation là gì?
Animation cho phép bạn tạo hoạt ảnh từ từ của một phần tử, từ trạng thái đầu đến trạng thái cuối (hoặc nhiều trạng thái ở giữa).
- 🎬 @keyframes: Định nghĩa các bước của hoạt ảnh
- ⏱️ animation-duration: Thời gian hoạt ảnh
- 🔄 animation-iteration-count: Số lần lặp lại
- ⏳ animation-delay: Trễ trước khi bắt đầu
@keyframes – Định nghĩa hoạt ảnh
@keyframes định nghĩa các bước của hoạt ảnh từ đầu (0%) đến cuối (100%).
/* Định nghĩa hoạt ảnh "slide" */
@keyframes slide {
0% {
/* Bắt đầu: ở vị trí trái */
left: 0;
background-color: red;
}
50% {
/* Giữa: ở vị trí giữa, màu vàng */
left: 250px;
background-color: yellow;
}
100% {
/* Kết thúc: ở vị trí phải, màu xanh */
left: 500px;
background-color: blue;
}
}
/* Áp dụng hoạt ảnh */
.box {
position: relative;
width: 50px;
height: 50px;
animation: slide 3s; /* Sử dụng hoạt ảnh "slide", thời gian 3 giây */
}Cú pháp @keyframes
@keyframes tên-hoạt-ảnh {
0% {
/* Trạng thái đầu */
}
50% {
/* Trạng thái giữa (tùy chọn) */
}
100% {
/* Trạng thái cuối */
}
}
/* Hoặc dùng from/to */
@keyframes slide {
from {
/* Giống 0% */
left: 0;
}
to {
/* Giống 100% */
left: 500px;
}
}Animation – Áp dụng hoạt ảnh
Sau khi định nghĩa @keyframes, bạn dùng animation để áp dụng nó cho một phần tử.
| Thuộc tính | Chức năng | Giá trị mặc định | Ví dụ |
|---|---|---|---|
animation-name | Tên hoạt ảnh (@keyframes) | none | animation-name: slide; |
animation-duration | Thời gian hoạt ảnh | 0s | animation-duration: 3s; |
animation-delay | Trễ trước khi bắt đầu | 0s | animation-delay: 0.5s; |
animation-iteration-count | Số lần lặp lại (infinite = lặp vô hạn) | 1 | animation-iteration-count: infinite; |
animation-timing-function | Tốc độ (linear, ease, ease-in…) | ease | animation-timing-function: linear; |
animation-direction | Hướng (normal, reverse, alternate) | normal | animation-direction: alternate; |
animation-fill-mode | Trạng thái sau hoạt ảnh (forwards, backwards) | none | animation-fill-mode: forwards; |
/* Cách dài */
.box {
animation-name: slide;
animation-duration: 3s;
animation-delay: 0.5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
animation-direction: alternate;
}
/* Shorthand */
.box {
animation: slide 3s 0.5s infinite ease-in-out alternate;
/* name duration delay iteration-count timing-function direction */
}animation-direction – Hướng hoạt ảnh
| Giá trị | Mô tả | Ví dụ |
|---|---|---|
normal | 0% → 100% (trái sang phải) | Mũi tên chạy từ trái sang phải |
reverse | 100% → 0% (phải sang trái) | Mũi tên chạy từ phải sang trái |
alternate | 0% → 100% → 0% (chiều đi, chiều về) | Mũi tên chạy qua lại |
alternate-reverse | 100% → 0% → 100% (chiều về, chiều đi) | Mũi tên chạy qua lại (bắt đầu ngược) |
animation-fill-mode – Trạng thái sau hoạt ảnh
| Giá trị | Mô tả | Khi nào dùng |
|---|---|---|
none (mặc định) | Trở về trạng thái ban đầu sau hoạt ảnh | Hoạt ảnh lặp lại (infinite) |
forwards | Giữ trạng thái cuối (100%) sau hoạt ảnh | Hoạt ảnh kết thúc, phần tử ở trạng thái cuối |
backwards | Giữ trạng thái đầu (0%) trước hoạt ảnh | Ít dùng |
both | Kết hợp forwards và backwards | Hoạt ảnh có delay |
/* Ví dụ: animation-fill-mode */
/* ❌ SAI: Phần tử quay lại vị trí ban đầu sau hoạt ảnh */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.box {
animation: fadeIn 1s;
/* Sau 1s, opacity quay lại 0 */
}
/* ✅ ĐÚNG: Phần tử giữ opacity 1 sau hoạt ảnh */
.box {
animation: fadeIn 1s forwards;
/* Sau 1s, opacity vẫn là 1 */
}Ví dụ 1 – Loading spinner (Biểu tượng tải)
<!-- HTML -->
<div class="spinner"></div>
<p>Đang tải...</p>/* CSS */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}Ví dụ 2 – Fade in & Fade out
<!-- HTML -->
<div class="fade-in">Fade In</div>
<div class="fade-out">Fade Out</div>
<div class="bounce">Bounce</div>/* Fade In */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn 2s ease-in forwards;
}
/* Fade Out */
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fade-out {
animation: fadeOut 2s ease-out forwards;
}
/* Bounce (nhảy) */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
.bounce {
animation: bounce 1s ease infinite;
}Ví dụ 3 – Pulse (Nhấp nháy)”
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}Ví dụ hoàn chỉnh – Trang web với animations
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Animation Demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Header với fadeIn -->
<header class="header">
<h1 class="fade-in-title">🎬 CSS Animation</h1>
<p class="fade-in-subtitle">Tạo hoạt ảnh tuyệt vời</p>
</header>
<!-- Loading spinner -->
<div class="loading">
<div class="spinner"></div>
<p>Đang tải...</p>
</div>
<!-- Bounce items -->
<section class="features">
<h2>Tính năng</h2>
<div class="feature-grid">
<div class="feature-card bounce">
<h3>⚡ Nhanh</h3>
<p>Animation chạy mượt mà</p>
</div>
<div class="feature-card bounce" style="animation-delay: 0.2s;">
<h3>💡 Dễ dùng</h3>
<p>CSS thuần túy, không JS</p>
</div>
<div class="feature-card bounce" style="animation-delay: 0.4s;">
<h3>✨ Đẹp</h3>
<p>Hiệu ứng chuyên nghiệp</p>
</div>
</div>
</section>
<!-- Pulse button -->
<div class="button-section">
<button class="btn-pulse">Nhấp vào tôi</button>
</div>
<!-- Floating animation -->
<div class="floating-box">
<span>↑</span>
</div>
</body>
</html>/* FILE: styles.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
}
/* ========== KEYFRAMES ========== */
/* Fade In Title */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Fade In Subtitle */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Loading spinner */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Bounce animation */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
/* Pulse animation */
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(52, 152, 219, 0);
}
}
/* Floating animation */
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
}
/* ========== STYLES ========== */
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 60px 20px;
text-align: center;
}
.fade-in-title {
font-size: 48px;
margin-bottom: 10px;
animation: fadeInDown 0.8s ease;
}
.fade-in-subtitle {
font-size: 20px;
opacity: 0.9;
animation: fadeInUp 0.8s ease 0.2s backwards;
}
/* Loading section */
.loading {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 200px;
gap: 20px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading p {
color: #666;
font-weight: bold;
}
/* Features section */
.features {
max-width: 1200px;
margin: 60px auto;
padding: 0 20px;
}
.features h2 {
text-align: center;
margin-bottom: 40px;
color: #2c3e50;
font-size: 32px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
}
.feature-card {
background: white;
padding: 30px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: bounce 1s ease infinite;
}
.feature-card h3 {
font-size: 24px;
margin-bottom: 15px;
color: #667eea;
}
.feature-card p {
color: #666;
line-height: 1.6;
}
/* Button section */
.button-section {
display: flex;
justify-content: center;
margin: 60px 0;
}
.btn-pulse {
padding: 15px 40px;
font-size: 18px;
background-color: #3498db;
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: bold;
animation: pulse 2s ease-in-out infinite;
}
.btn-pulse:hover {
animation-play-state: paused;
/* Tạm dừng animation khi hover */
}
/* Floating box */
.floating-box {
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
animation: float 3s ease-in-out infinite;
z-index: 50;
}Một số animation phổ biến
/* Slide in từ trái */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Rotate in */
@keyframes rotateIn {
from {
transform: rotate(-200deg);
opacity: 0;
}
to {
transform: rotate(0);
opacity: 1;
}
}
/* Flip */
@keyframes flip {
0% {
transform: perspective(400px) rotateY(0);
}
100% {
transform: perspective(400px) rotateY(360deg);
}
}
/* Shake (rung) */
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-5px);
}
20%, 40%, 60%, 80% {
transform: translateX(5px);
}
}
/* Heartbeat (nhịp tim) */
@keyframes heartbeat {
0%, 100% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(1);
}
}animation-play-state – Tạm dừng/Tiếp tục animation
/* Tạm dừng animation khi hover */
.spinner {
animation: spin 2s linear infinite;
}
.spinner:hover {
animation-play-state: paused;
/* Hoặc running để tiếp tục */
}Tóm tắt
- ✅ @keyframes: Định nghĩa hoạt ảnh (0%, 50%, 100%)
- ✅ animation-name: Tên hoạt ảnh
- ✅ animation-duration: Thời gian hoạt ảnh
- ✅ animation-delay: Trễ trước khi bắt đầu
- ✅ animation-iteration-count: Số lần lặp (infinite = vô hạn)
- ✅ animation-timing-function: Tốc độ (linear, ease, ease-in…)
- ✅ animation-direction: Hướng (normal, reverse, alternate)
- ✅ animation-fill-mode: Trạng thái sau hoạt ảnh (forwards, backwards)
- ✅ animation-play-state: Tạm dừng/tiếp tục (paused, running)
- ✅ Shorthand: animation: name duration delay count timing direction fill-mode
🎯 Bài tập thực hành
Bài tập 1: Tạo loading spinner với animation:
- Vòng tròn border (4px, màu xanh ở top)
- Xoay 360 độ liên tục (animation: spin 1s linear infinite)
- Thêm text “Đang tải…” bên dưới
- Styling đẹp với background gradient
Bài tập 2: Tạo bounce card animation:
- 3-4 thẻ (card) hiển thị với bounce animation
- Mỗi card có animation-delay khác nhau (0s, 0.2s, 0.4s…)
- Card: dịch lên, xuống (translateY) từ 0 đến -20px
- Animation duration: 1s, infinite, ease
- Mỗi card có icon/emoji, tiêu đề, mô tả
Bài tập 3: Tạo trang landing page với multiple animations:
- Header: fadeInDown animation
- Subtitle: fadeInUp animation với delay
- Features: bounce animation với staggered delay (0.2s, 0.4s, 0.6s)
- CTA button: pulse animation
- Floating icon: float animation (moveY up-down)
- Tất cả timing functions khác nhau
Chúc bạn thành công! 🚀