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

9 nút back to top button css — hiện khi cuộn

Nút back to top button css là nút nhỏ đưa bạn về đầu trang khi đã cuộn xuống sâu. Chín kiểu dưới đây chạy từ bản không AOS·GSAP, 0 dòng JS đến bản tự đọc vị

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

Chín mục không xếp theo bảng chữ cái mà theo đúng quãng đời của một nút trên màn hình. Trước hết là cách nút xuất hiện (01~03), kế đến hai kiểu hình khối đậm chất ý tưởng (04·06), rồi một hướng vào khác hẳn (05), cách né khỏi vùng footer (07), phản ứng khi bạn rê chuột lên (08), và cuối cùng là giao việc di chuyển cho trình duyệt mà không cần animation (09). Theo tinh thần scroll to top button css only, JavaScript nếu có cũng chỉ đọc vị trí cuộn, còn mọi chuyển động đều thuộc về CSS.

Mục Kỹ thuật Thời lượng Kích hoạt
01 Hiện dần khi cuộn opacity+translateY 260ms cuộn (JS)
02 Mũi tên nảy liên tục translateY 1.4s luôn chạy
03 Vòng tiến trình đọc stroke-dashoffset 2s (demo) cuộn (JS)
04 Hình tên lửa translateY+rotate 700ms hover·focus
05 Trượt vào từ cạnh translateX+@starting-style 320ms cuộn (JS)
06 Chữ quay quanh nút rotate 8s luôn chạy
07 Dừng trên footer translateY 220ms cuộn (JS)
08 Pill giãn khi hover width 220ms hover·focus
09 scroll-behavior (cuộn gốc của trình duyệt) — nhấp

01Hiện dần khi cuộn

Nút lên đầu trang xuất hiện khi nào

Nút nằm ẩn với opacity:0, và chỉ khi cuộn vượt quá 240px thì bảy dòng JavaScript mới gắn lớp .is-visible để nút trồi lên một cách từ tốn.

Hiện dần khi cuộn
Mở trong tab mới

01 · Hiện dần khi cuộn — Diện tích đo được 1.764% · 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

scroll 이벤트opacity+translateY260ms

Điểm đáng ghi nhớ

Đây là kiểu gặp nhiều nhất ở phần dưới trang danh sách dài hoặc bài blog, nơi người đọc thường xuyên muốn quay về đầu.

$duration: 260ms;
$easing: ease-out;

.totop {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity $duration $easing, transform $duration $easing;
}
.totop.is-visible { opacity: 1; transform: translateY(0); }
var btn = document.querySelector('.totop');
function sync() { btn.classList.toggle('is-visible', window.scrollY > 240); }
window.addEventListener('scroll', sync, { passive: true });
sync();

02Mũi tên nảy liên tục

Những gì bạn nhìn thấy

Nền nút giữ nguyên, riêng biểu tượng mũi tên bên trong cứ nảy lên xuống không ngừng.

Mũi tên nảy lên
Mở trong tab mới

02 · Mũi tên nảy lên — Diện tích đo được 0.17% · 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

translateY$ease-out1.4s

Cách làm ra

Chỉ với một keyframes CSS và hoàn toàn không có JavaScript, kiểu này vẫn báo hiệu không cần lời rằng nhấn vào đây là bạn sẽ đi lên.

.totop__arrow {
  animation: arrow-bounce 1.4s ease-out infinite;
}
@keyframes arrow-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

03Vòng tiến trình đọc

Bối cảnh của mục này là gì

Một vòng SVG ôm sát viền nút, và stroke-dashoffset tô dần vòng ấy đúng theo tỷ lệ phần trang bạn đã cuộn qua.

Vòng tiến trình tròn
Mở trong tab mới

03 · Vòng tiến trình tròn — Diện tích đo được 0.454% · 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

stroke-dashoffset@propertyJS 7줄

Đọc code như thế nào

Điểm mấu chốt là đăng ký @property --p với inherits: true để giá trị thay đổi trên nút cha được vòng SVG con kế thừa nguyên vẹn.

@property --p {
  syntax: '<number>';
  initial-value: 0;
  inherits: true;
}
.totop { --p: 0; }
.totop__fill {
  stroke-dasharray: 150.8px;
  stroke-dashoffset: calc(150.8px * (1 - var(--p) / 100));
}
var max = document.documentElement.scrollHeight - window.innerHeight;
var ratio = max > 0 ? Math.min(100, (window.scrollY / max) * 100) : 0;
el.style.setProperty('--p', ratio);

04Hình tên lửa

Điều gì đang diễn ra

Chính nút là biểu tượng tên lửa; khi bạn rê chuột lên hoặc focus bằng bàn phím, thân tên lửa nghiêng đi nhô lên nhẹ nhàng, còn đuôi lửa phía sau chớp tắt liên hồi.

Hình tên lửa
Mở trong tab mới

04 · Hình tên lửa — Diện tích đo được 0.289% · 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

rotate+translateYopacity 깜빡임700ms

Điểm chính trong code

Nếu bạn đang sưu tầm back to top button design css dạng hình khối thì mục 04 và 06 là hai kiểu đáng xem nhất, rất hợp với landing page của game hay startup cần một chút vui nhộn.

.totop:hover .totop__rocket,
.totop:focus-visible .totop__rocket { animation: rocket-launch 700ms ease-out 1; }
.totop:hover .totop__flame,
.totop:focus-visible .totop__flame { animation: flame-flicker 700ms ease-out 1; }

@keyframes rocket-launch {
  0%   { transform: translateY(0) rotate(0deg); }
  40%  { transform: translateY(-9px) rotate(-9deg); }
  100% { transform: translateY(-2px) rotate(-4deg); }
}
@keyframes flame-flicker {
  0%, 100% { opacity: 0; transform: scaleY(.6); }
  40%      { opacity: 1; transform: scaleY(1.3); }
  70%      { opacity: .7; transform: scaleY(.9); }
}

05Trượt vào từ cạnh

Trên màn hình xảy ra chuyện gì

Nút lướt vào từ bên ngoài mép phải của màn hình, tạo ấn tượng nhập cuộc khác hẳn kiểu trồi lên từ dưới của mục 01.

Trượt vào từ bên cạnh
Mở trong tab mới

05 · Trượt vào từ bên cạnh — Diện tích đo được 2.232% · 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

translateX@starting-style320ms

Nguyên lý hoạt động

Nhờ @starting-style đặt giá trị khởi đầu cho lúc phần tử vừa thoát khỏi display: none, JavaScript chỉ còn việc kiểm tra vị trí cuộn còn hình thức xuất hiện do CSS đảm nhận trọn gói.

.totop {
  display: none;
  opacity: 0;
  transform: translateX(40px);
  transition: opacity 320ms, transform 320ms, display 320ms allow-discrete;
}
.totop.is-visible {
  display: grid;
  opacity: 1;
  transform: translateX(0);
  @starting-style { opacity: 0; transform: translateX(40px); }
}

06Chữ quay quanh nút

Nhìn như thế nào

Dòng chữ "SCROLL TO TOP" chạy vòng theo viền nút nhờ SVG textPath, quay chầm chậm mãi không ngừng, trong khi mũi tên ở giữa đứng im một chỗ.

Chữ chạy vòng quanh
Mở trong tab mới

06 · Chữ chạy vòng quanh — Diện tích đo được 0.895% · 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

SVG textPathrotate8s

Cách dựng nên

Kiểu này hợp với những site có cá tính thương hiệu mạnh hoặc nút nằm cuối trang portfolio.

.totop__ring {
  animation: ring-spin 8s linear infinite;
}
@keyframes ring-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
<text><textPath href="#ring-path">SCROLL TO TOP · SCROLL TO TOP · </textPath></text>

07Dừng trên footer

Khi nào thì dùng

Nút vẫn nổi bám theo màn hình như thường lệ, nhưng khi footer kéo lên gần, nó tự nhấc mình lên trước bằng translateY để không che khuất footer.

Dừng lại trên footer
Mở trong tab mới

07 · Dừng lại trên footer — Diện tích đo được 24.521% · 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

IntersectionObservertranslateYJS 9줄

Vì sao dựng như vậy

Chín dòng IntersectionObserver chỉ đo chiều cao chồng lấn rồi ghi vào biến CSS --lift; còn phần chuyển động vẫn do một mình transform gánh vác.

.totop {
  transform: translateY(calc(-1 * var(--lift, 0px)));
  transition: transform 220ms ease-out;
}
new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    var overlap = Math.max(0, e.intersectionRect.height);
    btn.style.setProperty('--lift', overlap + 'px');
  });
}, { threshold: [0, 0.01, 1] }).observe(footer);

08Pill giãn khi hover

Màn hình trông ra sao

Lúc bình thường là một nút tròn chỉ có biểu tượng; rê chuột lên là nút giãn dài ra bên cạnh và lộ thêm dòng chữ về đầu trang.

Nút pill giãn khi hover
Mở trong tab mới

08 · Nút pill giãn khi hover — Diện tích đo được 5.592% · 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

width transition:hover·:focus-visible220ms

Cốt lõi của cách làm

Chuyển đổi width can thiệp vào bố cục nhiều hơn những giá trị như top hay left, nhưng vì trình duyệt chỉ phải vẽ lại đúng một nút nên chi phí rất nhỏ.

.totop {
  width: 48px;
  overflow: hidden;
  transition: width 220ms ease-out;
}
.totop:hover,
.totop:focus-visible { width: 132px; }

09scroll-behavior cho chuyển động mượt

Cái gì đang chuyển động

Khác tám mục phía trên, mục này chẳng có animation cũng chẳng có JavaScript.

Trượt mượt bằng scroll-behavior
Mở trong tab mới

09 · Trượt mượt bằng scroll-behavior — Diện tích đo được 50.129% · 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

href=#mp-topJS 0줄앵커 링크

Cái gì là mấu chốt

Thêm đúng một dòng html với scroll-behavior: smooth là trình duyệt tự trượt màn hình mượt mà lên đỉnh khi bạn nhấn vào neo. Trong mảng smooth scroll to top css, đây là nền tảng nhất: mọi mục phía trước đều có thể đứng cạnh nó.

html { scroll-behavior: smooth; }

.totop {
  text-decoration: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
<a href="#top" class="totop" aria-label="Lên đầu trang">
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <polyline points="5,15 12,8 19,15" />
  </svg>
</a>

Nút lên đầu trang dễ hỏng ở đâu — hai cái bẫy

Vấn đề nằm ở chỗ nào

Khi chia back to top button css thành chín phần, ngay bản đầu tiên của mục 03 tôi từng đăng ký --p với inherits: false và toàn bộ diện tích tích lũy gãy ngay ở mức 0.00%. Animation chạy trên nút cha .totop, nhưng thứ thật sự vẽ vòng là stroke-dashoffset nằm ở vòng SVG con .totop__fill; vì inherits: false nên con không nhận nổi giá trị mới của cha và vòng cứ đứng im ở mức 0%. Đổi sang inherits: true là mọi thứ chạy ngay.

Né như thế nào

Mục 07 cũng ẩn một bẫy tương tự: demo không có cuộn thật nên buộc footer-mock và nút phải chuyển động bằng keyframes chung một nhịp; chỉ cần một bên lệch nhịp là nút trông như xuyên thẳng qua footer. Zip mở bằng mật khẩu 9sbsj52d chứa sẵn bản vanilla và bản React của cả chín mục với duration và easing đã khớp nhau từng chút một.

Khả năng tiếp cận

Khi bật reduced-motion thì còn lại gì

Nếu hệ điều hành đang bật chế độ giảm chuyển động, cả chín mục đều có khối @media (prefers-reduced-motion: reduce) xóa phần chuyển động. Với những mục mà chính sự xuất hiện hay vị trí mang thông tin như 01·05·07, trạng thái cuối cùng vẫn được giữ nguyên nên nút không biến mất; còn các vòng lặp trang trí như 02·03·04·06 bị xóa đi nhưng chức năng nhấn vẫn nguyên vẹn. Riêng mục 09, chính scroll-behavior: smooth đã là giá trị trình duyệt sẵn tôn trọng cài đặt reduced-motion nên không cần xử lý thêm. Phạm vi hỗ trợ của media query này xem được trong tài liệu prefers-reduced-motion của MDN, còn mức hỗ trợ của scroll-behavior thì kiểm tra tại caniuse.

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

Các bộ khác của series gom tại chuyên mục CSS animation; nếu bạn muốn thêm chỉ báo phản ứng theo cuộn thì xem cả 9 chỉ báo tiến trình cuộn. Muốn biết site này do ai dựng thì trang giới thiệu ghi rõ cách thức làm ra.

FAQ — hỏi đáp nhanh

Nút về đầu trang nên xuất hiện sau bao nhiêu px cuộn?

Không có con số cố định, nhưng quanh nửa chiều cao viewport là mức được dùng nhiều nhất. Demo trong bài ghim ở 240px; trên trang thật nên tính theo window.innerHeight để dù màn hình to hay nhỏ, nút vẫn hiện ra ở cùng một tỷ lệ.

Dùng mỗi scroll-behavior như mục 09 thì còn cần các mục khác làm gì?

Vẫn cần chứ. scroll-behavior: smooth chỉ trả lời câu hỏi lên đỉnh bằng cách nào; chuyện nút xuất hiện lúc nào, hay có đè lên footer hay không vẫn phải giải quyết riêng. Mục 09 không cạnh tranh với 01~08, và cách dùng phổ biến nhất là chọn một kiểu xuất hiện trong 01~08 rồi kết hợp scroll-behavior cho chính hành động cuộn.

Trong React component cũng dùng nguyên văn IntersectionObserver chứ?

Đúng vậy, bản React của mục 07 (StopFooterButton) vẫn tạo đúng IntersectionObserver đó bên trong useEffect để theo dõi phần tử footer. Điểm khác duy nhất so với bản vanilla là nó gọi disconnect() để dọn dẹp khi component unmount.

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.