Tạo hiệu ứng Parallax hoàn toàn bằng CSS với Scroll-driven Animations

Công nghệ02 tháng 6, 2026·5 phút đọc

Hiệu ứng Parallax từng yêu cầu các trình lắng nghe sự kiện JavaScript phức tạp để tính toán vị trí trên từng khung hình. Tuy nhiên, với tính năng CSS Scroll-driven Animations mới, giờ đây chúng ta có thể đạt được hiệu ứng này chỉ bằng một khối mã CSS khai báo đơn giản, mang lại hiệu suất tốt hơn nhờ chạy ngoài luồng chính.

Tạo hiệu ứng Parallax hoàn toàn bằng CSS với Scroll-driven Animations

Hiệu ứng Parallax có một lịch sử lâu đời trong thiết kế web. Mặc dù có vô số cách và thư viện để thực hiện nó, một phương pháp hoàn toàn mới sử dụng CSS thuần đã trở nên khả thi nhờ vào CSS Scroll-driven animation timelines (Các dòng thời gian hoạt ảnh điều khiển bởi cuộn trang).

Trước đây, công thức phổ biến là sử dụng trình lắng nghe sự kiện cuộn (scroll event listener) trong JavaScript, tính toán lại vị trí trên từng khung hình và dịch chuyển phần tử lên xuống. Giờ đây, các hoạt ảnh điều khiển bởi cuộn trang có thể xử lý tất cả những việc đó chỉ bằng CSS.

Việc xử lý hoạt ảnh Parallax bằng CSS mang lại một số lợi thế rõ rệt: hiệu suất nên tốt hơn vì nó chạy ngoài luồng chính (off the main thread), nhưng điểm tôi thích nhất là sự đơn giản — toàn bộ quy trình trở thành một khối mã khai báo nhỏ gọn, có thể áp dụng chỉ với một lớp tiện ích (utility class).

Dưới đây là đoạn mã hoàn chỉnh cho lớp này:

.parallax {
  view-timeline-name: --parallax-tl;
  view-timeline-axis: block;
  overflow: hidden;

  & > * {
    scale: calc(1 + var(--parallax-offset, 20) * 2 / 100);
    animation: parallax auto linear both;
    animation-timeline: --parallax-tl;
    animation-range: cover;
    will-change: translate;
  }
}

@keyframes parallax {
  from {
    translate: 0 calc(var(--parallax-offset, 20) * -1%);
  }
  to {
    translate: 0 calc(var(--parallax-offset, 20) * 1%);
  }
}

Cơ chế Timeline

Bí quyết ở đây nằm ở view-timeline-name. Nó tạo ra một view progress timeline (dòng thời gian tiến trình khung nhìn), một dòng thời gian mà tiến trình của nó được đo bằng khoảng cách mà phần tử .parallax đã di chuyển qua khung cuộn (scrollport). Nó sẽ đọc 0% ngay khi phần tử bắt đầu đi vào khung nhìn và đạt 100% khi phần tử đã hoàn toàn rời đi.

Thuộc tính view-timeline-axis: block báo cho nó theo dõi chuyển động dọc theo trục khối (block axis), tương ứng với chiều dọc trong chế độ viết thông thường.

Trên phần tử con, animation-timeline: --parallax-tl sẽ chuyển đổi đồng hồ đếm của hoạt ảnh từ thời gian sang dòng thời gian đó. Từ đó, các thông số còn lại của hoạt ảnh sẽ tự động căn chỉnh:

  • auto cho thời lượng (duration), vì bây giờ thời lượng đến từ dòng thời gian thay vì một số giây cụ thể.
  • linear để tiến trình cuộn ánh xạ thẳng vào chuyển động.
  • both để giữ các khung hình bắt đầu và kết thúc bên ngoài phạm vi hoạt động.

⚠️ Lưu ý: Thuộc tính viết tắt animation-timeline không phải là một phần của thuộc tính viết tắt animation và phải được khai báo riêng biệt. Hơn nữa, animation-timeline phải được khai báo sau thuộc tính viết tắt animation vì viết tắt sẽ đặt lại các thuộc tính dài không được bao gồm về giá trị ban đầu của chúng.

Các khung hình chính (keyframes) thực hiện công việc thực sự. Với độ dịch chuyển mặc định, phần tử con sẽ trượt từ translate: 0 -20% sang translate: 0 20% khi bạn cuộn qua nó. Vì nó di chuyển với tốc độ khác so với vùng chứa xung quanh, bạn sẽ có được cảm giác về chiều sâu.

Tỷ lệ co giãn để tránh khoảng trống

Phần tử con dịch chuyển một lượng bằng tỷ lệ phần trăm độ dịch chuyển so với chiều cao của chính nó theo mỗi hướng, vì vậy nếu phần tử con có kích thước chính xác bằng vùng chứa của nó, việc dịch chuyển nó lên hoặc xuống sẽ để lộ một dải khoảng trống.

Phần tử con cần được phóng to lên để có thêm khoảng không gian để di chuyển vào. Nó cần thêm chiều cao tương đương với độ dịch chuyển ở cả trên và dưới, tức là gấp đôi độ dịch chuyển tổng cộng:

scale: calc(1 + var(--parallax-offset, 20) * 2 / 100);

Với độ dịch chuyển mặc định là 20, phần tử con sẽ được hiển thị ở kích thước 140% so với kích thước gốc. Phần thừa sẽ bị cắt bỏ bởi overflow: hidden trên vùng chứa, và luôn có đủ nội dung để bao phủ khung bất kể nó nằm ở đâu trong phạm vi di chuyển ±20%.

Điểm thú vị là cả translatescale đều đọc cùng một biến --parallax-offset. Tăng độ dịch chuyển để có hiệu ứng mạnh hơn và tỷ lệ co giãn sẽ tăng theo để phù hợp, giúp việc bao phủ luôn chính xác mà không cần can thiệp thủ công. Chỉ cần điều chỉnh một giá trị duy nhất, và các khoảng trống sẽ không bao giờ xuất hiện.

Cuối cùng, will-change: translate là mảnh ghép cuối cùng, một gợi ý rằng thuộc tính translate của phần tử này sắp thay đổi để trình duyệt có thể nâng cấp nó lên lớp riêng (layer) của nó trước.

Tôn trọng tùy chọn chuyển động

Parallax là chuyển động gắn liền với việc cuộn trang, và một số người dùng thích không có hiệu ứng này. Đó là một thực hành tốt để tôn trọng điều đó bằng cách tắt hoạt ảnh cho bất kỳ ai có cài đặt prefers-reduced-motion: reduce. Trong trường hợp này, chúng ta có thể chỉ cần tắt hoạt ảnh và tỷ lệ co giãn:

@media (prefers-reduced-motion: reduce) {
  .parallax > * {
    animation: none;
    scale: 1;
  }
}

Với cách tiếp cận mới này, các nhà phát triển web có thể tạo ra những trải nghiệm mượt mà và hiện đại mà không làm nặng nề hiệu năng của trang web.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗