9 hiệu ứng css button animation copy-paste
CSS button animation là chuyển động nhỏ trên nút, cho người dùng biết nút đã ghi nhận thao tác: hover, nhấn, chờ, lỗi hoặc xác nhận.
Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip
- 01 Gợn sóng
- 02 Nhấc lên
- 03 Gạch chân trượt
- 04 Nam châm
- 05 Viền xoay
- 06 Chuyển sang loading
- 07 Mũi tên đẩy
- 08 Rung lắc
- 09 Giữ để xác nhận
Thứ tự chín hiệu ứng dưới đây không phải "phổ biến nhất trước". Đó là thứ tự một nút bấm trải qua trong vòng đời của nó: được chú ý (01 đến 05), phản hồi khi bị nhấn (06 và 07), thừa nhận một lỗi (08), rồi hỏi hai lần trước khi làm việc nguy hiểm (09). Lưới 3×3 phía trên tự chạy lặp trong khung, không cuộn được, nên bạn chỉ cần xem, không cần bấm gì cả. Mỗi mục dưới đây chỉ giữ lại phần code cốt lõi, bản đầy đủ nằm trong file zip, và mỗi file trong zip chạy độc lập, không phụ thuộc file khác.
01Gợn sóng
Một vòng tròn gợn sóng lan ra từ điểm bạn nhấn rồi biến mất. Dùng cho nút thanh toán hoặc xác nhận, nơi bạn cần người dùng cảm nhận rõ ràng là đã nhấn trúng.
.btn {
position: relative; overflow: hidden; isolation: isolate;
&::after {
content: ""; position: absolute;
left: var(--x, 50%); top: var(--y, 50%);
width: var(--d, 0); height: var(--d, 0);
border-radius: 50%; background: $ripple;
transform: translate(-50%, -50%) scale(0); opacity: 0;
}
&.is-rippling::after { animation: ripple $duration $easing forwards; }
}
@keyframes ripple { to { transform: translate(-50%, -50%) scale(1); opacity: 0; } }
02Nhấc lên
Nút nhấc lên 6px khi bạn hover, đồng thời bóng đổ cũng lớn dần theo. Hợp với nút dạng card hoặc nút thêm vào giỏ hàng, chỗ nào nút cần cảm giác như một vật bạn có thể cầm lên.
$lift: 6px; $duration: 220ms; $shadow: rgba(23, 20, 26, .32);
.btn {
will-change: transform;
transition: transform $duration cubic-bezier(.2, .8, .2, 1), box-shadow $duration;
&:hover, &:focus-visible { transform: translateY(-$lift); box-shadow: 0 14px 24px $shadow; }
&:active { transform: translateY(-2px); box-shadow: 0 6px 12px $shadow; }
}
03Gạch chân trượt
Gạch chân kéo từ trái sang phải khi bạn hover, và khi rời chuột thì trượt ra bên phải. Dùng cho nút dạng link văn bản và thanh điều hướng.
.link::after {
content: ""; position: absolute; left: 0; right: 0; bottom: 0;
height: $thickness; background: currentColor;
transform: scaleX(0); transform-origin: right;
transition: transform $duration cubic-bezier(.2, .8, .2, 1);
}
.link:hover::after { transform: scaleX(1); transform-origin: left; }
04Nam châm
Khi con trỏ đến gần trong một bán kính nhất định, nút bị hút nhẹ về phía con trỏ. Dùng cho CTA duy nhất trên trang landing, vì hai nút kiểu này trên cùng một màn hình sẽ biến trang thành món đồ chơi.
document.addEventListener('pointermove', function (e) {
var r = btn.getBoundingClientRect();
var dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
if (Math.hypot(dx, dy) < radius) btn.style.transform = 'translate(' + dx * strength + 'px,' + dy * strength + 'px)';
else btn.style.transform = '';
});
05Viền xoay
Viền gradient xoay liên tục quanh nút không ngừng. Dùng cho nút duy nhất cần trông đặc biệt, như tính năng AI hay premium.
.btn {
position: relative; overflow: hidden; isolation: isolate;
padding: $border; border-radius: 16px;
&::before {
content: ""; position: absolute; inset: -60%;
background: conic-gradient(from 0deg, $colors);
animation: spin $duration linear infinite; z-index: -1;
}
}
.btn__in { display: block; border-radius: 13px; background: #17141a; }
@keyframes spin { to { transform: rotate(360deg); } }
06Chuyển sang loading
Khi bạn nhấn, chữ trên nút mờ dần rồi biến mất, spinner hiện lên thay vào đó. Dùng cho nút lưu hoặc gửi, và giữ nguyên độ rộng để phần tử bên cạnh không bị xô lệch.
.btn { display: grid; place-items: center; min-width: 200px; }
.btn__label, .btn__spin { grid-area: 1 / 1; transition: opacity $duration, transform $duration; }
.btn__spin {
width: 22px; height: 22px; border-radius: 50%;
border: 3px solid rgba(255, 255, 255, .35); border-top-color: #fff;
opacity: 0; transform: scale(.6); animation: spin $spin linear infinite;
}
.btn.is-loading .btn__label { opacity: 0; transform: translateY(6px); }
.btn.is-loading .btn__spin { opacity: 1; transform: scale(1); }
07Mũi tên đẩy
Khi bạn hover, đường mũi tên dài ra và đầu mũi tên bị đẩy sang phải. Dùng cho nút tiếp theo, xem thêm, tiếp tục, vì mũi tên chỉ là một đường thẳng và một góc xoay nên không cần icon font.
.arrow__line { transform-origin: left; transform: scaleX(.55); transition: transform $duration; }
.arrow__head {
width: 12px; height: 12px;
border-top: 2.5px solid currentColor; border-right: 2.5px solid currentColor;
transform: rotate(45deg); transition: transform $duration;
}
.btn:hover .arrow__line { transform: scaleX(1); }
.btn:hover .arrow__head { transform: rotate(45deg) translate($push * .7, -$push * .7); }
08Rung lắc
Khi có lỗi, nút rung trái phải ba lần rồi dừng lại. Dùng cho đăng nhập thất bại hoặc nhập sai, vì rung quá năm lần sẽ khiến người dùng thấy như nút đang giận dữ.
.btn.is-shaking { animation: shake $duration cubic-bezier(.36, .07, .19, .97) both; }
@keyframes shake {
10%, 90% { transform: translateX(-$amount * .5); }
20%, 80% { transform: translateX($amount); }
30%, 70% { transform: translateX(-$amount); }
40%, 60% { transform: translateX($amount * .7); }
50% { transform: translateX(-$amount * .7); }
}
09Giữ để xác nhận
Khi bạn giữ nút, nền được lấp đầy dần, còn thả sớm thì nền tụt về lại vị trí cũ. Dùng cho xóa hoặc hủy tài khoản, thay cho modal xác nhận. JS thêm class is-holding khi pointerdown và bỏ đi khi pointerup, sự kiện animationend của phần fill là tín hiệu để thực thi hành động.
.btn__fill {
position: absolute; inset: 0; background: $fill;
transform: scaleX(0); transform-origin: left; z-index: -1;
transition: transform 200ms ease-out;
}
.btn.is-holding .btn__fill { animation: fill $hold linear forwards; }
@keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
Chỗ dễ hỏng — cái bẫy nằm ở đâu?
Cả 9 hiệu ứng chỉ animate transform và opacity, đó là chủ đích chứ không phải tình cờ. Animate width hay top buộc trình duyệt phải tính lại layout ở từng frame, còn transform và opacity được xử lý gọn trong bước compositing. Hướng dẫn hiệu năng animation của MDN khuyên dùng đúng hai thuộc tính này vì lý do đó. Cái bẫy dễ mắc nhất nằm ở mục 06: thu nhỏ nút trong lúc loading phá vỡ quy tắc này và đẩy các phần tử bên cạnh xô lệch, nên hãy giữ nguyên độ rộng và chỉ đổi phần bên trong.
Cái bẫy thứ hai là kích thước. Lần đầu dựng mục 01, kích thước vòng gợn sóng đặt tới 320px trên một nút nhỏ, và vòng tròn phủ kín cả nút chỉ sau hai frame, nên trông như một cú lóe sáng chứ không phải một đợt sóng lan ra. Hãy tính đường kính gợn sóng bằng hai lần cạnh dài hơn của nút, và đừng để dưới 600ms. Những giá trị đó đã nằm sẵn trong cả 9 file rồi, và mật khẩu của file nén là xu8ewsq8 nếu bạn muốn lấy ngay bây giờ.
Cái bẫy thứ ba là thao tác chạm. Mục 02, 03 và 07 đều dựa vào hover, thứ không tồn tại trên điện thoại. Các file trong zip gắn cùng chuyển động đó vào :focus-visible, còn trên thiết bị cảm ứng, trạng thái :active thay cho hover.
Khả năng tiếp cận
Cả 9 hiệu ứng đều xử lý prefers-reduced-motion: reduce. Quy tắc chung là bỏ chuyển động nhưng vẫn giữ phản hồi: mục 02 giữ bóng đổ mà không nhấc lên, mục 06 hiển thị một vòng tĩnh thay vì quay, mục 08 đổi màu thay vì rung. Mục 09 là ngoại lệ, vì giữ để xác nhận là một cơ chế an toàn chứ không phải trang trí, nên phần fill vẫn giữ nguyên. Mức hỗ trợ trình duyệt cho media query này được liệt kê trên MDN prefers-reduced-motion.
@media (prefers-reduced-motion: reduce) {
.btn.is-demo, .btn.is-shaking { animation: none; }
.btn:hover { transform: none; }
}
Bạn có thể xem thêm các bộ sưu tập khác về nút và loader trong chuyên mục CSS animation, và đọc thêm về trang này tại trang giới thiệu.
FAQ
Bạn có thể dùng cả 9 hiệu ứng trong một dự án không?
Được, nhưng chỉ nên giữ tối đa hai kiểu trên cùng một màn hình. Mục 04 (nam châm) và 05 (viền xoay) là hiệu ứng kiểu "một nút đặc biệt duy nhất", vì vậy mỗi trang chỉ nên dùng mỗi loại một lần. Các mục còn lại gắn với tình huống cụ thể nên hiếm khi đụng nhau.
React thay thế việc toggle class như thế nào?
Các component trong thư mục react/ của file zip nhận state qua props: loading cho mục 06, một bộ đếm lỗi shakeKey cho mục 08, và callback onConfirm cho mục 09. Các module SCSS chỉ đọc giá trị var(), nên bạn truyền ba biến đó dưới dạng props.
Animation bị giật thì nên kiểm tra gì trước?
Trước tiên, hãy chắc chắn không có thuộc tính nào ngoài transform và opacity đang animate. Sau đó thêm will-change: transform chỉ cho phần tử đang chuyển động, vì đặt nó lên mọi thứ sẽ làm chậm đi chứ không nhanh hơn.