GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

9 css confetti animation — dán một file là chạy

Bạn hẳn đã gặp css confetti animation ít nhất một lần: giấy màu bắn ra khoảnh khắc đăng ký hay thanh toán xong.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục không xếp theo độ hoa mỹ mà theo thứ tự confetti thực tế xuất hiện trên màn hình. Bắt đầu là tín hiệu bắn ra từ một cú nhấn nút (01) và tín hiệu rơi liên tục trên nền (02), nâng lên lớp nhấn mạnh khi chính nút và chữ tiêu đề lấp lánh (03, 04), qua nhóm phản ứng dạng nổ cho cú đánh lớn hơn (05, 06), rồi tới cách triển khai không thêm markup hay không cần JavaScript (07, 08) và tương tác bám theo chuột (09).

01Nút confetti bắn tung

Dùng khi nào

Bấm nút hoàn tất đăng ký hay thanh toán, 12 mảnh confetti nhỏ bay tứ tán theo góc rải sẵn bởi vòng lặp @for của SCSS, rồi mờ dần biến mất.

Bão giấy nổ từ nút bấm
Mở trong tab mới

01 · Bão giấy nổ từ nút bấm — Diện tích đo được 2.569% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@for 루프translate+rotate900ms

Vì sao dựng như vậy

Bấm lần nữa, bảy dòng JavaScript tháo rồi gắn lại lớp để keyframes phát từ đầu.

@for $i from 1 through 12 {
  .cb-piece:nth-child(#{$i + 1}) {
    background: nth($piece-colors, ($i - 1) % 4 + 1);
    --angle: #{$i * 30deg};
    --dist: #{64px + ($i % 3) * 22px};
    animation-delay: #{($i % 4) * 40ms};
  }
}

@keyframes cb-fly {
  0%   { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0) scale(.4); opacity: 0; }
  15%  { opacity: 1; }
  100% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(-1 * var(--dist))) scale(1); opacity: 0; }
}

02Mưa confetti

Màn hình là gì

Trên banner trúng thưởng, 18 mảnh rơi với chiều cao (top) và tốc độ khác nhau.

Mưa confetti
Mở trong tab mới

02 · Mưa confetti — Diện tích đo được 12.233% · khung hình chuyển động 22/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@for 루프translateY2.4s

Cốt lõi phần dựng

Nếu không xáo thứ tự chiều cao lệch khỏi thứ tự vị trí ngang, các mảnh sẽ dồn thành một hàng và xuất phát trên cùng một đường chéo, nên tôi cố tình rải lệch thứ tự đó.

$tops: -40px, 160px, -180px, 20px, 100px, -120px, 60px, -200px, 140px, -20px, 180px, -100px, 40px, -160px, 120px, -60px, 200px, -140px;
@for $i from 1 through 18 {
  .cr-piece:nth-child(#{$i + 1}) {
    left: percentage($i / 19);
    top: nth($tops, $i);
    background: nth($piece-colors, $i % 4 + 1);
    animation-duration: #{1700ms + ($i % 5) * 140ms};
  }
}

@keyframes cr-fall {
  0%   { transform: translateY(0) rotate(0deg); }
  100% { transform: translateY(520px) rotate(300deg); }
}

03Nút lấp lánh

Hiệu ứng lấp lánh css dùng ở đâu

Đưa chuột lên nút premium, bốn ngôi sao nhỏ quanh viền phồng lên rồi xẹp xuống theo animation-delay khác nhau.

Nút lấp lánh
Mở trong tab mới

03 · Nút lấp lánh — Diện tích đo được 0.213% · khung hình chuyển động 22/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleopacitystaggered delay

Điểm mấu chốt là gì

Rời chuột ra là hiệu ứng dừng ngay, không đi qua transition, nên màn hình không bị chớp.

@keyframes sk-twinkle {
  0%, 100% { opacity: 0; transform: scale(.3) rotate(0deg); }
  50%      { opacity: 1; transform: scale(1) rotate(90deg); }
}

// Dùng thật: chỉ lấp lánh khi hover. Bản quay is-demo thì luôn bật
.sk-wrap:hover .sk-star,
.sk-wrap.is-demo .sk-star { animation: sk-twinkle $duration $easing infinite; }
.sk-star--2 { animation-delay: 160ms; }
.sk-star--3 { animation-delay: 320ms; }
.sk-star--4 { animation-delay: 480ms; }

04Chữ lấp lánh

Dùng ở đâu

Ba đốm lấp lánh đặt quanh chữ tiêu đề chúc mừng, mỗi đốm phồng lên xẹp xuống tại thời điểm bắt đầu khác nhau.

Chữ lấp lánh
Mở trong tab mới

04 · Chữ lấp lánh — Diện tích đo được 0.193% · khung hình chuyển động 21/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleopacity@keyframes twinkle

Điều đáng lưu ý

Nhân vật chính ở đây là chữ chứ không phải nút, nên ngay cả trên nền trắng, mắt người xem vẫn hướng vào tiêu đề trước.

.st-star {
  position: absolute;
  color: $color;
  font-size: 18px;
  line-height: 1;
  opacity: 0;
  transform: scale(.2);
  pointer-events: none;
}
.st-star--1 { top: -14px; left: -18px; color: $stage-orange; }
.st-star--2 { top: -20px; right: -14px; }
.st-star--3 { bottom: -16px; left: 40%; color: $stage-orange; }

@keyframes st-twinkle {
  0%, 100% { opacity: 0; transform: scale(.2) rotate(0deg); }
  50%      { opacity: 1; transform: scale(1) rotate(70deg); }
}

.st-wrap.is-demo .st-star { animation: st-twinkle $duration $easing infinite; }
.st-star--2 { animation-delay: 260ms; }
.st-star--3 { animation-delay: 480ms; }

05Pháo hoa chấm tròn

Điều mắt thấy

Một chấm tách thành tám chấm chia nhau tám hướng của custom property --angle, chỉ dùng rotate(var(--angle)) và translateY để tỏa ra như pháo hoa rồi mờ đi.

Chấm pháo hoa
Mở trong tab mới

05 · Chấm pháo hoa — Diện tích đo được 1.597% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

custom propertyrotate()+translateY1s

Cách tạo ra nó

Markup chỉ có tám thẻ span rỗng nên mọi phép tính vị trí đều do SCSS lo.

// Tám chấm chia nhau --angle từ 30 đến 360deg, tỏa ra như pháo hoa bằng rotate + translateY
@for $i from 1 through 8 {
  .fw-dot:nth-child(#{$i}) {
    background: nth($piece-colors, ($i - 1) % 4 + 1);
    --angle: #{$i * 45deg};
  }
}

@keyframes fw-burst {
  0%   { transform: rotate(var(--angle)) translateY(0) scale(1); opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: rotate(var(--angle)) translateY(-92px) scale(.3); opacity: 0; }
}

06Emoji bật lên

Tình huống nào

Bấm nút cổ vũ, ba emoji nhảy vọt lên rồi biến mất theo keyframes chồng translateY với scale, mỗi cái trễ một nhịp khác nhau.

Emoji bật lên
Mở trong tab mới

06 · Emoji bật lên — Diện tích đo được 1.865% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateY+scalestaggered delay700ms

Đọc code thế nào

Bấm lần nữa, đoạn JavaScript ngắn khởi động lại keyframes theo cùng cách mục 01.

@keyframes ep-pop {
  0%   { transform: translateY(0) scale(.4); opacity: 0; }
  30%  { opacity: 1; }
  100% { transform: translateY(-64px) scale(1.1); opacity: 0; }
}

// Dùng thật: cú click gắn lại is-pop (xem thẻ script phía trên)
.ep-wrap.is-pop .ep-emoji,
.ep-wrap.is-demo .ep-emoji { animation: ep-pop $duration $easing infinite; }
.ep-emoji--2 { animation-delay: 110ms; }
.ep-emoji--3 { animation-delay: 220ms; }

07Confetti chỉ bằng pseudo element

Chuyện gì xảy ra

Card hoàn tất không thêm lấy một phần tử vào markup; chỉ hai pseudo ::before và ::after vẽ mỗi cái sáu chấm bằng box-shadow rồi thả rơi.

Confetti chỉ bằng pseudo-element
Mở trong tab mới

07 · Confetti chỉ bằng pseudo-element — Diện tích đo được 2.233% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

::before/::afterbox-shadow 다중점1.2s

Điểm chính của code

Hai pseudo này chỉ lệch nhau vị trí bắt đầu và độ trễ nên không đè lên nhau.

// Không thêm phần tử nào vào markup — ::before và ::after mỗi cái vẽ 6 chấm bằng box-shadow
.pc-card::before,
.pc-card::after {
  content: "";
  position: absolute;
  top: -18px;
  left: 14px;
  width: 6px;
  height: 6px;
  border-radius: $r-pill;
  background: $color;
  box-shadow:
    22px -8px 0 -1px $stage-yellow,
    44px 6px  0 0  $stage-orange,
    66px -12px 0 -1px $subject-cream,
    88px 4px  0 0  $color,
    110px -6px 0 -1px $stage-yellow,
    132px 8px 0 0  $subject-cream;
  opacity: 0;
}

08Trigger ăn mừng bằng :has(:checked)

Trên màn hình diễn ra gì

Tick checkbox đánh dấu hoàn tất việc, label cha đọc trạng thái qua :has(:checked) và bật cùng lúc tám mảnh confetti, không cần một dòng JavaScript.

Kích hoạt chúc mừng bằng :has(:checked)
Mở trong tab mới

08 · Kích hoạt chúc mừng bằng :has(:checked) — Diện tích đo được 1.599% · khung hình chuyển động 13/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

:has():checked800ms

Nguyên lý hoạt động

Bỏ tick là điều kiện trigger thành sai, keyframes không chạy lại.

@keyframes hc-fly {
  0%   { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0) scale(.4); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(-58px) scale(1); opacity: 0; }
}

.hc-wrap:has(.hc-check:checked) .hc-piece,
.hc-wrap.is-demo .hc-piece { animation: hc-fly $duration $easing infinite; }

09Vệt lấp lánh bám con trỏ

Nhìn thấy thế nào

Rê chuột trên banner, tọa độ lưu thành giá trị pixel --mx·--my, sáu đốm lấp lánh bám theo điểm ấy bằng translate() cộng thêm offset riêng.

Vệt lấp lánh theo con trỏ
Mở trong tab mới

09 · Vệt lấp lánh theo con trỏ — Diện tích đo được 1.717% · khung hình chuyển động 21/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

pointermovecustom propertytranslate()

Cách dựng

Bản demo quay headless dùng is-demo nên thay chuột bằng cách cho chính --mx·--my chạy vòng tròn.

@keyframes sc-twinkle {
  0%, 100% { transform: translate(calc(var(--mx) + var(--ox)), calc(var(--my) + var(--oy))) translate(-50%, -50%) scale(0); opacity: 0; }
  50%      { transform: translate(calc(var(--mx) + var(--ox)), calc(var(--my) + var(--oy))) translate(-50%, -50%) scale(1); opacity: 1; }
}

// Bản quay: is-demo cho chính --mx và --my chạy vòng tròn (headless không phát được pointermove)
@keyframes sc-drift {
  0%   { --mx: 44px;  --my: 40px; }
  25%  { --mx: 236px; --my: 34px; }
  50%  { --mx: 228px; --my: 140px; }
  75%  { --mx: 52px;  --my: 132px; }
  100% { --mx: 44px;  --my: 40px; }
}

.sc-wrap.is-demo { animation: sc-drift 2s linear infinite; }
.sc-wrap .sc-piece { animation: sc-twinkle $duration $easing infinite; }

Cái bẫy của confetti nằm ở đâu

Vấn đề là gì

Đúng như nghĩa gốc của confetti là giấy vụn bay đầy trời, mục 02 chính là chỗ vấp sớm nhất trong chín mục. Khi các mảnh chỉ khác nhau ở độ trễ lúc bắt đầu, chúng dồn lại thành một hàng và cùng lao trên một đường chéo; phải rải chiều cao bắt đầu (top) theo thứ tự khác với vị trí ngang thì cơn mưa mới giống mưa thật.

Tránh thế nào

Tra pure css confetti animation hay confetti css button thì ra nhiều ví dụ rời rạc, chín mục ở đây gom chung một trang cho bạn dán ngay. Thuộc tính :has() của mục 08 và @property của mục 09 khá mới, chạy tốt trên Chrome, Edge, bản mới Safari và Firefox, nhưng trên trình duyệt cũ thì cú kích hoạt không chạy, hoặc tọa độ nhảy bậc thay vì nội suy mượt. Khi giải nén, bjwrdptn chuỗi mật khẩu cần để mở tệp zip nằm lẫn trong chữ thường của đoạn này, đọc kỹ là thấy.

Khả năng tiếp cận — chín hiệu ứng giữ lại gì khi bật reduced-motion

Bật reduced-motion còn lại gì

Cài đặt giảm chuyển động của hệ điều hành bật lên thì cả chín demo đều dừng confetti qua khối @media (prefers-reduced-motion: reduce). Confetti trang trí như 01, 02, 05, 06, 07 dừng thành khung tĩnh là đủ; lớp lấp lánh của 03, 04 và vệt bám chuột của 09 vốn là trang trí chứ không mang thông tin, tắt đi thì nội dung nút và chữ vẫn nguyên vẹn. Mục 08 đọc trạng thái checkbox bằng :has(:checked) không liên quan animation nên dù giảm chuyển động, dấu hoàn tất vẫn còn.

Bài nên đọc tiếp

Muốn phản ứng dạng card tương tự, bạn đọc tiếp 9 hiệu ứng hover bento grid CSS; các bài sưu tầm khác nằm ở danh mục CSS animation.

FAQ — câu hỏi thường gặp

Pháo confetti bắt buộc phải có JavaScript mới nổ được sao?

Không. Mục 08 kích hoạt chỉ bằng trạng thái checkbox qua :has(:checked), không có lấy một dòng JavaScript; pháo hoa của 05 và confetti pseudo của 07 cũng tự lặp thuần CSS. Chỉ những mục cần click hay tọa độ chuột mới phải dùng đoạn JavaScript ngắn 6–8 dòng.

Mục Cách kích hoạt JavaScript
05, 07 CSS tự lặp không
08 :has(:checked) không
01, 06, 09 click, tọa độ chuột 6–8 dòng

Tăng số mảnh confetti có làm chậm không?

Khoảng 12–18 mảnh chỉ chạy transform và opacity nên không tính lại bố cục, đa số máy đều mượt. Đẩy lên hàng chục mảnh thì số lớp tổng hợp tăng theo, máy yếu có thể tụt khung hình, nên số mảnh hiện cùng lúc trên màn hình tốt nhất giữ quanh mức 20.

Code vanilla và React trong zip chạy ra kết quả khác nhau không?

Không, chuyển động như nhau. Bản vanilla đặt ba biến SCSS ở đầu file và gắn animation bằng class; bản React nhận giá trị đó qua props rồi đẩy sang CSS custom property trong inline style, khác nhau chừng đó thôi.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.