Hiệu ứng thị sai khi cuộn là một kỹ thuật hình ảnh, trong đó các lớp nội dung khác nhau di chuyển ở tốc độ khác nhau khi người dùng cuộn xuống một trang. Điều này tạo ra ảo giác về chiều sâu, trong đó các phần tử ở nền trước có vẻ di chuyển nhanh hơn các phần tử ở nền sau, mang lại trải nghiệm duyệt web hấp dẫn và sống động. Bạn có thể đạt được hiệu ứng này một cách hiệu quả nhất bằng cách sử dụng Hiệu ứng chuyển động theo thao tác cuộn trong CSS, cho phép bạn liên kết hiệu ứng chuyển động với vị trí cuộn của vùng chứa.
Cách triển khai
Sau đây là cách tạo hiệu ứng thị sai cơ bản:
Tạo phần tử bao bọc: Phần tử này chỉ đơn giản là nhóm tất cả các lớp của hiệu ứng thị sai lại với nhau. Đây không phải là phần tử có thể cuộn, vì vậy, phần tràn của phần tử này sẽ bị cắt bớt. Ngoài ra, hãy đặt cho nó một
heightkhớp với chiều cao của một trong các lớp của hiệu ứng thị sai.<div class="wrapper"> … </div>.wrapper { overflow: clip; height: 100vh; /* Height of one of the layers of the parallax */ }Khai báo các lớp: Bên trong trình bao bọc, hãy thêm các lớp riêng lẻ sẽ di chuyển ở các tốc độ khác nhau.
<div class="wrapper"> <div class="layer">LAYER 0</div> <div class="layer">LAYER 1</div> <div class="layer">LAYER 2</div> … </div>Thêm ảnh động dịch chuyển: Xác định ảnh động CSS thay đổi thuộc tính
transformcủa các lớp. Để tạo hiệu ứng thị sai, bạn thường sẽ sử dụngtranslateYđể di chuyển các lớp theo chiều dọc.@keyframes parallax { from { transform: translateY(700px); } }Thiết lập
view-timeline: Để liên kết ảnh động với vị trí cuộn, hãy tạo mộtview-timelinetrên phần tử trình bao bọc rồi áp dụng cho các lớp..wrapper { view-timeline: --wrapper; } .layer { animation: parallax linear both; animation-timeline: --wrapper; }Sắp xếp các ảnh động: Để các lớp di chuyển ở tốc độ khác nhau, bạn có thể sử dụng một trong hai phương pháp chính: sắp xếp trong khung hình chính hoặc sắp xếp
animation-range.Cả hai phương pháp này đều có thể sử dụng các giá trị được cố định giá trị trong mã hoặc có thể sử dụng cách triển khai
sibling-index()/sibling-count(). Các giá trị được cố định trong mã là dễ nhất và cũng hữu ích khi chỉ có một số lượng lớp hạn chế. Việc triển khaisibling-index()/sibling-count()sẽ rất hữu ích khi bạn có nhiều lớp.Sắp xếp so le trong khung hình chính:
Bằng cách sử dụng giá trị được mã hoá cứng, bạn có thể xác định một thuộc tính tuỳ chỉnh cho mỗi lớp để kiểm soát độ lệch thị sai theo cách thủ công.
.layer:nth-child(1) { --offset: 100px; } .layer:nth-child(2) { --offset: 200px; } .layer:nth-child(3) { --offset: 300px; } @keyframes parallax { from { transform: translateY(var(--offset)); } }Khi sử dụng
sibling-index(), hãy để hàmsibling-index()trả về chỉ mục của một phần tử con trong số các phần tử đồng cấp để tự động tính toán hiệu ứng so le.@keyframes parallax { from { transform: translateY(calc(100px * sibling-index())); } }Sắp xếp
animation-range:Bằng cách sử dụng giá trị được mã hoá cứng, bạn có thể xác định rõ ràng ranh giới của
animation-rangetrên từng lớp riêng lẻ..layer:nth-child(1) { animation-range: entry 25% exit 50%; } .layer:nth-child(2) { animation-range: entry 25% exit 75%; } .layer:nth-child(3) { animation-range: entry 25% exit 100%; }Khi sử dụng
sibling-index()vàsibling-count(), bạn có thể tính toán phạm vi theo toán học dựa trên tổng số lớp (sibling-count())..layer { animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index()); }
Mã mẫu
@keyframes parallax {
from {
transform: translateY(calc(100px * sibling-index()));
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
Ngoài ra, bạn có thể sử dụng thuộc tính animation-range để đạt được hiệu ứng tương tự:
@keyframes parallax {
from {
transform: translateY(700px);
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
Các phương pháp hay nhất
Khi sử dụng ảnh động dựa trên thao tác cuộn, bạn cần tuân thủ một số phương pháp hay nhất để đảm bảo trải nghiệm mượt mà và dễ tiếp cận:
- NÊN thêm tính năng phát hiện: Không phải trình duyệt nào cũng hỗ trợ ảnh động dựa trên thao tác cuộn. Sử dụng
@supports ((animation-timeline: view()) and (animation-range: entry))để kiểm tra khả năng hỗ trợ và cung cấp phương án dự phòng cho các trình duyệt không hỗ trợ.- BẮT BUỘC phải đưa quy trình kiểm tra
(animation-range: entry)vào đây để lọc ra những trình duyệt chỉ hỗ trợ một phần. - KHÔNG sử dụng gói
scroll-timeline-polyfillcho chiến lược dự phòng vì gói này chưa hoàn thiện tính năng và có nhiều vấn đề đã biết. - Nếu ảnh động chỉ được coi là để trang trí, hãy chọn Progressive Enhancement (Cải tiến tăng dần) và KHÔNG cung cấp phương án dự phòng.
- BẮT BUỘC phải đưa quy trình kiểm tra
- NÊN tôn trọng lựa chọn ưu tiên của người dùng: Một số người dùng muốn có ít chuyển động hơn trên web. Sử dụng truy vấn nội dung nghe nhìn
prefers-reduced-motionđể tắt hoặc giảm ảnh động cho những người dùng này. - NÊN cố gắng chỉ tạo ảnh động cho các thuộc tính CSS hiệu suất cao: Để có ảnh động mượt mà nhất, hãy chỉ tạo ảnh động cho các thuộc tính có thể được xử lý bằng luồng trình tổng hợp của trình duyệt, chẳng hạn như
transformvàopacity. Việc tạo ảnh động cho các thuộc tính khác nhưwidthhoặcheightcó thể dẫn đến các vấn đề về hiệu suất. - NÊN sử dụng đúng thứ tự khai báo: Khi sử dụng thuộc tính viết tắt
animation, hãy khai báoanimation-timelinevàanimation-rangesau thuộc tính đó để ngăn thuộc tính viết tắt đặt lại dòng thời gian.
Đối với việc đặt animation-range:
- NÊN đặt cùng một độ lệch bắt đầu cho tất cả các lớp, ví dụ:
entry 25% - NÊN đặt cho tất cả các lớp một độ lệch kết thúc khác nhau, sử dụng
sibling-count()vàsibling-index()để phân phối độ lệch, ví dụ:exit calc(100% / sibling-count() * sibling-index()).
Hỗ trợ trình duyệt và chiến lược dự phòng
Do đó, bạn thường cần có một chiến lược dự phòng.
Đối với những trình duyệt không hỗ trợ ảnh động dựa trên thao tác cuộn, bạn có thể sử dụng tính năng dự phòng để tạo lại hiệu ứng hình ảnh. Các phương án dự phòng thường được xây dựng bằng trình nghe cuộn (cho hiệu ứng ScrollTimeline) hoặc API IntersectionObserver (cho hiệu ứng ViewTimeline).
Trong các trình duyệt có hỗ trợ tích hợp cho ảnh động dựa trên thao tác cuộn, hãy LUÔN sử dụng phương thức triển khai CSS gốc vì các trình duyệt đó có hiệu suất cao hơn.
Xin lưu ý rằng không phải hiệu ứng nào cũng có thể được tạo lại bằng phương pháp dự phòng.
Cụ thể cho trường hợp sử dụng này, tập lệnh sau đây áp dụng phương án dự phòng cho các trình duyệt không hỗ trợ ảnh động dựa trên thao tác cuộn. Hàm này sử dụng IntersectionObserver để theo dõi chế độ hiển thị của phần tử .wrapper và cập nhật thuộc tính transform của các lớp dựa trên vị trí cuộn.
// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
const wrapper = document.querySelector('.wrapper');
const layers = document.querySelectorAll('.layer');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
window.addEventListener('scroll', onScroll);
} else {
window.removeEventListener('scroll', onScroll);
}
});
}, { threshold: 0 });
observer.observe(wrapper);
function onScroll() {
const scrollY = window.scrollY;
const wrapperRect = wrapper.getBoundingClientRect();
const wrapperTop = wrapperRect.top + scrollY;
const wrapperHeight = wrapperRect.height;
const windowHeight = window.innerHeight;
if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
layers.forEach((layer, index) => {
// This matches the effect as defined in the CSS example above.
// Customize this further if needed.
const initialTranslateY = 100 * index;
const translateY = initialTranslateY * (1 - scrollPercent);
layer.style.transform = `translateY(${translateY}px)`;
});
}
}
// Trigger onScroll once to set initial positions
onScroll();
}