Kể chuyện bằng thao tác cuộn

Scrollytelling là một kỹ thuật phổ biến được dùng để tạo ra trải nghiệm web hấp dẫn và sống động. Kỹ thuật này bao gồm việc tạo ảnh động cho các phần tử trên trang khi người dùng di chuyển, kể chuyện một cách hiệu quả hoặc hướng dẫn người dùng thông qua một câu chuyện. Với Ảnh động dựa trên thao tác cuộn CSS, bạn có thể tạo các hiệu ứng này ngay trong CSS mà không cần dựa vào JavaScript. Ảnh động được điều khiển bằng vị trí cuộn chứ không phải đồng hồ dựa trên thời gian, đảm bảo ảnh động luôn đồng bộ với thao tác cuộn của người dùng.

Cách triển khai

Để tạo trải nghiệm kể chuyện bằng thao tác cuộn, bạn cần có hai nhóm phần tử: một nhóm để theo dõi vị trí cuộn và một nhóm để tạo ảnh động.

Trước tiên, hãy xác định view-timeline được đặt tên trên các phần tử mà bạn muốn theo dõi. Các giá trị này sẽ đóng vai trò là trình điều khiển cho ảnh động.

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

Tiếp theo, hãy áp dụng ảnh động cho các phần tử mà bạn muốn tạo ảnh động và liên kết các phần tử đó với dòng thời gian mà bạn vừa tạo bằng thuộc tính animation-timeline.

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

Để animation-timeline có thể tham chiếu đến các dòng thời gian được đặt tên, chúng cần nằm trong cùng một phạm vi. Bạn có thể sử dụng thuộc tính timeline-scope trên một đối tượng cấp trên chung để cung cấp dòng thời gian cho tất cả các phần tử cần đến. Phần tử :root thường là một lựa chọn phù hợp cho việc này.

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

Cuối cùng, bạn có thể sử dụng thuộc tính animation-range để chỉ định phạm vi chính xác của dòng thời gian mà ảnh động sẽ chạy. Điều này giúp bạn kiểm soát chi tiết thời điểm kích hoạt ảnh động và cách ảnh động tiến triển.

#animated section {
  animation-range: entry 25% cover 50%, exit 50% exit 75%;
}

Mã mẫu

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

@keyframes animate-in {
  from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
  to { transform: rotateY(0deg); }
}
@keyframes animate-out {
  to { translate: 100% 0; opacity: 0; }
}

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
    backface-visibility: hidden;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

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: scroll()) and (animation-range: 0% 100%)) để 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: 0% 100%) 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-polyfill cho 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.
  • 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ư transform và opacity. Việc tạo ảnh động cho các thuộc tính khác như width hoặc height có 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áo animation-timeline và animation-range sau thuộc tính đó để ngăn thuộc tính viết tắt đặt lại dòng thời gian.

Khi sử dụng thuộc tính view-timeline để tạo ảnh động dựa trên thao tác cuộn:

  • NÊN sử dụng <dashed-ident> CSS cho tên.
  • KHÔNG BẮT BUỘC nêu rõ trục cần theo dõi: Khi không nhắm đến trục block mặc định (chẳng hạn như trong trình cuộn ngang), hãy nêu rõ trục cần theo dõi bằng view-timeline-axis.
  • NÊN đảm bảo phạm vi của hoạt động tra cứu hoạt động: Khi phần tử khai báo view-timeline không phải là phần tử cấp trên dạng cây phẳng của phần tử ảnh động, hãy nâng chế độ hiển thị tên của view-timeline bằng cách sử dụng timeline-scope trên một phần tử cấp trên được chia sẻ.

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 từng phần tử #tracked section và cập nhật thuộc tính transform của #animated section tương ứng.

const animatedSections = document.querySelectorAll('#animated section');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
    if (sectionIndex !== -1) {
      const animatedSection = animatedSections[sectionIndex];
      const ratio = entry.intersectionRatio;

      // Animate-in
      animatedSection.style.opacity = ratio;
      animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;

      // Animate-out
      if (ratio < 0.5) {
        animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
      } else {
        animatedSection.style.translate = '0 0';
      }
    }
  });
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });

document.querySelectorAll('#tracked section').forEach(section => {
  observer.observe(section);
});

Và CSS đi kèm:

#animated section {
  opacity: 0;
  transform: scale(0.5)  rotateY(-180deg);
  backface-visibility: hidden;
}

/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    opacity: 1 !important;
    transform: none !important;
    translate: 0 0 !important;
  }
}

Phương án dự phòng này cung cấp ảnh động dựa trên thao tác cuộn chính xác hơn cho các trình duyệt không hỗ trợ tính năng CSS gốc, đảm bảo trải nghiệm nhất quán hơn cho tất cả người dùng. Bằng cách sử dụng một loạt ngưỡng cho IntersectionObserver, chúng ta có thể theo dõi vị trí cuộn chính xác hơn và tạo ảnh động mượt mà hơn.