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

9 mẫu css pricing table animation copy-paste

Css pricing table là khối hiển thị các gói giá xếp cạnh nhau trên một trang. Chín animation cho bảng giá dưới đây viết gọn theo kiểu một dòng copy-paste là

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

Chín mẫu pricing table html css này xếp theo thứ tự khách dạo mắt qua một bảng giá: đầu tiên là tín hiệu lọt vào mắt ngay khi cuộn tới (01~03), tiếp là đoạn thao tác thật — đổi chu kỳ thanh toán (04~06), rồi đoạn soát chức năng kèm theo và so sánh các gói (07~08), cuối cùng dừng ở CTA dẫn tay tới nút thanh toán (09).

01Các thẻ gói trồi lên theo lượt khi cuộn

Điều bạn thấy

Cuộn xuống đoạn bảng giá, các thẻ gói không đổ ập vào một lúc mà lần lượt nổi lên từng chiếc theo thứ tự.

Gói giá hiện dần theo scroll
Mở trong tab mới

01 · Gói giá hiện dần theo scroll — Diện tích đo được 37.606% · khung hình chuyển động 12/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

IntersectionObservertransition-delayJS 7줄

Cốt lõi khi triển khai

IntersectionObserver bắt đúng khoảnh khắc cả lưới lọt vào màn hình để gắn class in, còn mỗi thẻ chỉ khác nhau một chút transition-delay nên cùng một cử động lặp lại nhưng lệch nhịp nhau.

var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    if (e.isIntersecting) { grid.classList.add('in'); io.unobserve(grid); }
  });
}, { threshold: 0.3 });
io.observe(grid);
.sr-card { opacity: 0; transform: translateY($sp-4); transition: opacity 400ms, transform 400ms; }
.sr-grid.in .sr-card { opacity: 1; transform: none; }
.sr-grid.in .sr-card:nth-child(2) { transition-delay: 140ms; }
.sr-grid.in .sr-card:nth-child(3) { transition-delay: 280ms; }

02Vệt sáng quét qua badge

Tình huống nào

Trên badge gói phổ biến, một dải sáng mảnh cứ vài giây lại lướt qua một lần, nên chưa cần hover mắt cũng bị kéo về phía nó tự nhiên.

Badge quét vệt sáng
Mở trong tab mới

02 · Badge quét vệt sáng — Diện tích đo được 1.628% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

::aftertranslateX2.4s

Điểm mấu chốt

Bên trong badge đã có overflow: hidden, chỉ riêng dải gradient dịch chuyển bằng transform: translateX, nên kích thước badge không hề rung chuyển.

.sh-badge { position: relative; overflow: hidden; }
.sh-badge::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 40%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .65), transparent);
  animation: sh-sweep 2.4s ease-in-out infinite;
}
@keyframes sh-sweep {
  0% { transform: translateX(-260%) skewX(-12deg); }
  60%, 100% { transform: translateX(360%) skewX(-12deg); }
}

03Thẻ gói nổi bật nhấc được nâng thêm

Chuyện gì xảy ra

Một css pricing card đã được đánh dấu bằng viền và badge, khi hover lên sẽ nâng cao thêm một tầng kèm bóng đổ đậm hơn.

Nhấc cao gói phổ biến
Mở trong tab mới

03 · Nhấc cao gói phổ biến — Diện tích đo được 5.008% · 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

translateYbox-shadow240ms

Điểm cần ghi nhớ

Chỉ có transform: translateY và box-shadow chuyển đổi cùng nhau, các thẻ nằm cạnh không bị kéo theo lay động.

.pl-card--popular {
  box-shadow: $shadow-press;
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.pl-card--popular:hover {
  transform: translateY(-#{$sp-4});
  box-shadow: $shadow-raised;
}

04Toggle hàng tháng·hàng năm

Trên màn hình

Giữa hai nút Monthly và Yearly, một nền hình viên tròn trượt mượt sang lại, báo cho biết chu kỳ thanh toán nào đang được chọn.

Toggle tháng/năm
Mở trong tab mới

04 · Toggle tháng/năm — Diện tích đo được 5.783% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

:checkedtranslateX50%

Cách tạo

Chỉ cần đọc trạng thái :checked của nút radio ẩn bằng selector anh em thông thường rồi dời indicator, không có một dòng javascript nào.

.mt-indicator {
  position: absolute;
  width: calc(50% - #{$sp-1});
  height: calc(100% - #{$sp-1} * 2);
  transition: transform $duration $easing;
}
#mt-yearly:checked ~ .mt-indicator { transform: translateX(100%); }

05Lật số giá

Nhìn thế nào

Đưa chuột lên số giá, thẻ xoay như một tấm thẻ học từ vựng và mặt sau ghi số tiền khác hiện ra.

Lật số giá
Mở trong tab mới

05 · Lật số giá — Diện tích đo được 9.398% · khung hình chuyển động 6/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

rotateXbackface-visibilityperspective

Cách đọc code

Bên trong một cha có perspective, hai mặt mang rotateX và backface-visibility: hidden chồng lên nhau, chỉ một lớp trong cùng được xoay.

.pf-flip__inner {
  transform-style: preserve-3d;
  transition: transform $duration $easing;
}
.pf-flip:hover .pf-flip__inner { transform: rotateX(180deg); }
.pf-flip__face { position: absolute; inset: 0; backface-visibility: hidden; }
.pf-flip__face--back { transform: rotateX(180deg); }

06Đổi giá với JS 0 bằng :has(:checked)

Dùng khi nào

Trông gần giống toggle phía trên, nhưng lần này trạng thái chọn của nút radio được đọc từ toàn bộ thẻ bằng :has(), để hai dòng giá hàng tháng·hàng năm cross-fade tự nhiên.

Đổi giá bằng :has(:checked), 0 JS
Mở trong tab mới

06 · Đổi giá bằng :has(:checked), 0 JS — Diện tích đo được 1.061% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

:has():checkedopacity

Điểm chính của code

Toàn bộ logic thay giá gói gọn trong một selector nên không cần tới javascript.

.hc-price { position: absolute; inset: 0; opacity: 0; transform: translateY($sp-2); }
.hc-price--monthly { opacity: 1; transform: none; }
.hc-card:has(.hc-radio--y:checked) .hc-price--monthly {
  opacity: 0;
  transform: translateY(-#{$sp-2});
}
.hc-card:has(.hc-radio--y:checked) .hc-price--yearly { opacity: 1; transform: none; }

07Dấu check hiện lần lượt

Màn hình nào

Hover vào thẻ, từng dòng check trong danh sách chức năng xuất hiện từ trên xuống, mỗi dòng cách nhau một nhịp.

Dấu check hiện lần lượt
Mở trong tab mới

07 · Dấu check hiện lần lượt — Diện tích đo được 2.008% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

transition-delayopacitytranslateX

Nguyên lý hoạt động

Icon check không vẽ bằng hai nét rời, mà lấy một phần tử uốn thành chữ L bằng border-right với border-bottom rồi xoay 45 độ, nên hai nét không bao giờ lệch nhau.

.fc-check {
  width: 6px; height: 10px;
  border-right: 2px solid $color;
  border-bottom: 2px solid $color;
  transform: rotate(45deg);
}
.fc-row { opacity: 0; transform: translateX(-#{$sp-3}); transition: opacity 320ms, transform 320ms; }
.fc-card:hover .fc-row { opacity: 1; transform: none; }
.fc-card:hover .fc-row:nth-child(2) { transition-delay: 120ms; }
.fc-card:hover .fc-row:nth-child(3) { transition-delay: 240ms; }

08Nổi cột trong bảng so sánh

So sánh gói giá dùng ở đâu?

Trong bảng dồn nhiều gói cạnh nhau, đúng cột đang được hover mới sáng viền lên và phình nhẹ, gói đang xem thành rõ ràng hẳn.

Highlight cột đang xem
Mở trong tab mới

08 · Highlight cột đang xem — Diện tích đo được 5.153% · 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

scaleborder-color180ms

Cách làm

Chỉ scale và border-color tham gia transition, bố cục của cột bên cạnh không bị xê dịch.

.ch-col {
  border: 2px solid transparent;
  transition: transform 180ms, border-color 180ms, background 180ms;
}
.ch-col:hover {
  transform: scale(1.05);
  border-color: $color;
  background: rgba(23, 20, 26, .08);
}

09CTA phát sáng

Dùng ở đâu?

Phía sau nút thanh toán dưới cùng bảng giá, một quầng sáng mềm phồng lên rồi xẹp xuống liên tục, lặng lẽ chỉ tay về chỗ cần bấm.

CTA phát sáng
Mở trong tab mới

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

radial-gradientscale1.8s

Vì sao làm vậy

Giống vệt sáng badge ở mục 02, đây là mục luôn chuyển động không cần hover, nên cường độ được hạ xuống dịu hơn hẳn các mục khác để không gây chói mắt.

.cg-btn::before {
  content: "";
  position: absolute;
  inset: -#{$sp-3};
  background: radial-gradient(circle, rgba(47, 109, 246, .9), transparent 65%);
  filter: blur(3px);
  animation: cg-pulse 1.8s $easing infinite;
}
@keyframes cg-pulse {
  0%, 100% { transform: scale(1); opacity: .2; }
  50% { transform: scale(1.45); opacity: .95; }
}

Animation css pricing table dễ vỡ ở đâu

Vấn đề là gì?

Đập chín mục cho một trang gói giá, chỗ vấp đầu tiên lại là mục 09. Muốn đẩy quầng sáng ra sau nút nên vô tư gán z-index: -1, nhưng .cg-btn không có z-index riêng nên không sinh stacking context mới: quầng sáng bị dìm dưới cả nền sân khấu gần nhất và biến mất hoàn toàn. Bỏ z-index đi, pseudo-element mới trở về đúng chỗ — phía trên nền của chính nút.

Tránh thế nào?

Selector :has() ở mục 06 cũng cần để ý: Chrome, Edge, Safari và Firefox mới chạy tốt, còn trình duyệt cũ bỏ qua cả quy tắc nên giá đứng yên ở lần áp dụng cuối — dữ liệu không mất, chỉ hết hiệu ứng chuyển, trong thực tế không nguy hiểm lắm. Mật khẩu cần để mở file zip đã nằm ở chỗ prqmpcmj bằng những chữ bình thường ngay trong bản văn, đọc chậm đoạn này là ra.

Tiếp cận — chín demo giữ lại gì dưới reduced-motion

Dưới reduced-motion còn lại gì?

Khi hệ thống bật giảm chuyển động, cả chín mục đều tắt animation và transition trong prefers-reduced-motion: reduce. Những mục vốn chạy vĩnh viễn không cần hover như vệt sáng badge (02) hay quầng CTA (09) chỉ việc đứng yên, không mất thông tin; những mục mà việc xuất hiện tự nó là thông tin như 01 và 07 được ghim opacity: 1 từ đầu để thẻ và danh sách check hiện đủ ngay. Mục 04, 05, 06 quan trọng ở kết quả giá trị đổi (phí, giá), nên chỉ bỏ tốc độ chuyển còn trạng thái cuối vẫn hiển thị nguyên.

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

Muốn xem thêm cách dùng radio với :has() để điều khiển trạng thái, có bài CSS toggle switch; danh sách đầy đủ nằm ở mục CSS animation. Phạm vi hỗ trợ chính xác của :has() kiểm tra trong tài liệu MDN.

FAQ — hỏi đáp nhanh

Thêm animation vào trang bảng giá có thật sự tăng tỉ lệ chuyển đổi không?

Chính animation không quyết định việc mua. Nhưng khi làm pricing table design html css mà gắn thêm chuyển động tự nhiên vào những yếu tố vốn đã muốn nhấn — gói phổ biến, nút thanh toán — ánh mắt sẽ ghé tới đó sớm hơn.

Nên dùng toggle mục 04 hay cách :has() mục 06?

Màn hình đơn giản chỉ cần đổi hình dạng nút mà giá giữ nguyên thì toggle mục 04 đã đủ. Nếu con số giá hàng tháng·hàng năm phải đổi thật tại chỗ thì làm theo mục 06, dùng :has() khép phần đổi giá trong CSS và không phải thêm một dòng javascript nào.

Code vanilla với react trong zip có khác nhau không?

Chuyển động nhìn thấy là như nhau. Khác mỗi cấu trúc: bản vanilla dùng ba biến SCSS ($duration $easing $color hoặc giá trị tương đương theo mục), bản React nhận đúng các giá trị đó qua props rồi chuyển thành CSS custom property bên trong component.

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.