Scroll-driven animation timelines
Một UI pattern phổ biến bao gồm các phần tử animate khi người dùng cuộn theo chiều dọc hoặc ngang trên trang. Các scroll-driven animation này xảy ra trực tiếp khi cuộn trang hoặc một scroll container tràn trong trang.
Các thuộc tính được định nghĩa trong module CSS scroll-driven animations mở rộng CSS animations bằng cách cho phép animate các giá trị thuộc tính được định nghĩa trong animation @keyframes để phản hồi tương tác người dùng.
Hướng dẫn này cung cấp tổng quan về cách dùng CSS để tạo scroll-driven animation timeline và animation.
Scroll-driven animation là gì?
Module CSS scroll-driven animations định nghĩa các thuộc tính cho phép CSS keyframe animation được liên kết với cuộn.
Tiến triển của timeline
Animation có thể được đặt để tiến triển theo scroll-based timeline thay vì timeline tài liệu dựa trên thời gian mặc định, mà không cần JavaScript. CSS cho phép chúng ta xác định animation timeline nào sẽ dùng, bao gồm animate các phần tử bằng cách cuộn một phần tử có thể cuộn thay vì bằng sự trôi qua của thời gian.
Lợi ích về hiệu suất
CSS scroll-driven animation có hiệu suất tốt. JavaScript scroll-driven animation yêu cầu scroll event listener và các đối tượng IntersectionObserver trên main thread để theo dõi các phần tử qua scrollport. Bất cứ khi nào bạn dựa vào main thread để render hiệu ứng bằng JavaScript, bạn có nguy cơ chặn main thread, có thể dẫn đến trang không phản hồi và trải nghiệm người dùng kém, hoặc jank.
Nền tảng
Scroll-driven animation xây dựng trên CSS animations và Web Animations API. Trước khi tạo scroll-driven animation, bạn phải hiểu về animation @keyframes trong CSS. Xem hướng dẫn sử dụng CSS animation để tìm hiểu thêm.
Trong CSS, animation được tạo bằng cách gắn keyframe animation vào một phần tử dùng thuộc tính animation-name (hoặc shorthand animation). Theo mặc định, animation chạy trên timeline tài liệu mặc định, di chuyển từ keyframe from đến keyframe to theo thời gian trôi qua, với animation kéo dài bằng thời gian được định nghĩa bởi giá trị thuộc tính animation-duration. Khi được đặt để chạy trên timeline tài liệu mặc định, animation chạy đến hoàn thành trừ khi bị ngăn không cho vậy, ví dụ bằng cách có animation-play-state được đặt thành paused hoặc bằng cách xóa animation-name khỏi phần tử.
Scroll-driven animation là CSS animation không chạy trên DocumentTimeline mặc định. Thay vào đó, chúng chạy trên scroll-progress hoặc view-progress timeline, được điều khiển bởi cuộn nội dung của phần tử. Có một liên kết trực tiếp giữa hành động cuộn của người dùng và tiến triển của animation qua các keyframe @keyframe. Khi người dùng cuộn lên, xuống, trái hoặc phải, animation di chuyển tiến hoặc lùi qua tiến triển keyframe. Khi cuộn tạm dừng, animation cũng tạm dừng, như thể animation-play-state được đặt thành pause.
Animation timeline
Thuộc tính animation-timeline, được định nghĩa trong module CSS animations, được dùng để đặt timeline cho animation.
Module CSS scroll-driven animations định nghĩa các tính năng để đặt animation-timeline là scroll-progress hoặc view-progress timeline. Bạn có thể tường minh đặt tên một phần tử là timeline controller dùng các thuộc tính scroll-timeline-* và view-timeline-*, rồi đặt tên đó làm animation-timeline của phần tử con. Bạn cũng có thể định nghĩa anonymous scroll progress timeline và anonymous view progress timeline dùng các hàm scroll() và view().
Ngoài ra, thuộc tính animation-timeline có thể được dùng để tường minh xác định rằng timeline tài liệu mặc định được dùng hoặc để chỉ định rằng animation không có timeline, và do đó không nên xảy ra.
CSS animation thông thường: timeline tài liệu mặc định
Đặt animation-timeline tường minh thành auto, hoặc bỏ qua thuộc tính và để nó mặc định thành auto, đặt timeline là timeline tài liệu mặc định. Khi được đặt thành giá trị mặc định này, tiến triển của animation được xác định bởi animation-duration, animation-delay, và lượng thời gian đã trôi qua kể từ khi animation được liên kết với phần tử qua thuộc tính animation-name. Timeline dựa trên thời gian là timeline truyền thống liên quan đến CSS animation.
:checked ~ .container > .item {
animation-name: action;
animation-duration: 3s;
animation-delay: 500ms;
animation-timeline: auto;
}
Chúng ta tạo animation keyframe xoay được gọi là action:
@keyframes action {
from {
rotate: 45deg;
}
to {
rotate: 765deg;
}
}
Khi checkbox được chọn, animation action được áp dụng cho phần tử. Khi bỏ chọn, animation không được áp dụng cho <div>.
Hãy thử chọn checkbox. Không có gì xảy ra trong nửa giây animation delay. Sau đó, khi animation bắt đầu, hộp sẽ nhảy đến góc xoay 45 độ, và sau đó mất 3 giây để xoay thêm 720 độ, hoặc thêm hai vòng đầy đủ. Sau tổng cộng ba giây rưỡi, animation kết thúc và <div> sẽ trở về trạng thái không xoay mặc định.
Note:
animation-timeline được đặt lại thành giá trị mặc định auto bởi shorthand animation, nhưng không thể được đặt bằng shorthand. Do đó, khi tạo scroll-driven animation, luôn khai báo animation-timeline sau bất kỳ khai báo shorthand animation nào để đạt hiệu ứng mong muốn.
Scroll progress timeline
Với scroll progress timeline, timeline tiến triển dựa trên việc cuộn phần tử có thể cuộn (scroller) từ trên xuống dưới (hoặc trái sang phải) và ngược lại. Theo mặc định, vị trí trong phạm vi cuộn được chuyển đổi thành phần trăm tiến triển — 0% ở đầu và 100% ở cuối.
Để tạo scroll progress timeline, giá trị animation-timeline phải tham chiếu đến scroller, có thể được đặt tên hoặc ẩn danh.
Named scroll progress timeline
Named scroll progress timeline là timeline trong đó scroller được đặt tên tường minh bằng thuộc tính scroll-timeline-name (hoặc shorthand scroll-timeline). Tên là một <dashed-ident>. Scroller được liên kết với phần tử cần animate bằng cách chỉ định scroll-timeline-name của nó làm giá trị của thuộc tính animation-timeline của phần tử đó.
HTML của chúng ta bao gồm ba phần tử: item, phần tử chúng ta sẽ animate; container, phần tử chúng ta sẽ cuộn; và scroller. container cần đủ lớn để tràn ra ngoài scroller cha của nó: Nếu không có cuộn, sẽ không có scroll timeline.
<main class="scroller">
<div class="container">
<span class="item"></span>
</div>
</main>
Chúng ta cung cấp một số style cơ bản. Những style quan trọng bao gồm đặt chiều cao cho container lớn hơn scroller, rồi đặt overflow để cho phép cuộn:
.scroller {
width: 400px;
height: 100px;
overflow: scroll;
}
.container {
height: 200px;
}
Đặt animation-timeline trên phần tử được animate khớp với scroll-timeline-name của một phần tử tổ tiên là điều tạo ra named scroll progress timeline. Chúng ta cũng phải bao gồm animation, điều chúng ta làm bằng cách đặt giá trị thành phần animation-name của shorthand animation thành tên <custom-ident> của animation keyframe của chúng ta:
.scroller {
scroll-timeline-name: --rotate;
}
.item {
animation: action 1ms linear;
animation-timeline: --rotate;
}
Trong trường hợp này, chúng ta không có checkbox, vì tiến triển animation action được kiểm soát bởi việc cuộn scroller tràn, không giống như thời gian, không hết hạn.
Trước khi cuộn xảy ra, vị trí của container nằm ở đầu scroller và animation ở keyframe 0%. Hãy thử cuộn xuống. Khi bạn cuộn, animation tiến qua timeline, xoay thêm 720 độ. Khi bạn không thể cuộn nữa, tiến triển animation ở 100%, hoặc keyframe to. Phần tử được animate không trở về góc xoay mặc định trừ khi scroller được cuộn trở lại lên trên.
Thời gian animation
Bạn có thể đã chú ý rằng thành phần animation-duration của shorthand animation được đặt thành 1ms. Khi tạo CSS scroll-driven animation, việc chỉ định giá trị animation-duration không ảnh hưởng đến thời lượng của animation và không cần thiết. Tuy nhiên, thời lượng có thể ảnh hưởng đến animation timeline view progress phi tuyến tính, và Firefox yêu cầu animation-duration khác không để áp dụng animation cho phần tử. Vì những lý do này, thực hành phổ biến là đặt animation-duration thành 1ms.
Đặt animation-duration: 1ms đảm bảo rằng animation hoạt động trong Firefox, hiệu ứng animation nhất quán trên tất cả trình duyệt, và animation bị ẩn nếu trình duyệt không hỗ trợ view progress animation timeline. Nếu trình duyệt hỗ trợ keyframe animation, animation sẽ không hiển thị với người dùng. Tuy nhiên, animation vẫn xảy ra và các animation event được kích hoạt.
Anonymous scroll progress timeline
Bạn không cần đặt tên cho scroll progress timeline. Thay vào đó, bạn có thể liên kết anonymous scroll progress timeline với animation. Trong trường hợp này, animation-timeline của phần tử cần animate được đặt thành hàm scroll(). Hàm này chọn scroller cung cấp scroll progress timeline và trục cuộn để sử dụng dựa trên các đối số tùy chọn bạn truyền vào. Một tham số là từ khóa <scroller> xác định mối quan hệ của phần tử scroller với phần tử hiện tại (nearest, root hoặc self). Tham số kia là giá trị <axis> của thanh cuộn (block, inline, y hoặc x).
Ví dụ này sử dụng tất cả CSS giống như ví dụ trước, ngoại trừ animation-timeline, được chúng ta đặt thành hàm scroll(). Chúng ta cũng ghi đè kích thước của container để thay đổi hướng cuộn:
.item {
animation: action 1ms linear;
animation-timeline: scroll(nearest inline);
}
.container {
inline-size: 800px;
block-size: 100%;
}
Chúng ta đặt inline-size trên container để nó tràn theo chiều inline, và đặt block-size thành 100% để nó không còn tràn theo chiều block. Hãy thử cuộn theo chiều inline.
View progress timeline
Bạn cũng có thể tiến hành animation dựa trên sự thay đổi về khả năng hiển thị của một phần tử bên trong scroller — điều này được thực hiện thông qua view progress timeline. Thay vì theo dõi scroll offset của scroll container, view progress timeline theo dõi vị trí tương đối của một phần tử, được gọi là subject, trong một scrollport. Sự tiến triển của các keyframe animation dựa trên khả năng hiển thị của subject bên trong scroller. Khác với scroll progress timeline, với view progress timeline, bạn không thể chỉ định scroller — khả năng hiển thị của subject luôn được theo dõi trong scroll container tổ tiên gần nhất của nó.
Animation view progress timeline chỉ xảy ra khi phần tử hiển thị trong scrollport. Tiến triển timeline bắt đầu ở 0% khi subject được theo dõi bắt đầu giao cắt với scrollport ở cạnh cuối block hoặc inline. 100% xảy ra khi subject thoát khỏi scrollport ở cạnh bắt đầu block hoặc inline.
Vì 100% thường đạt được khi phần tử rời khỏi viewport, bạn có thể muốn đặt hiệu ứng cuối cùng của animation trong khối keyframe xảy ra trước khi animation kết thúc. Bạn có thể đặt hiệu ứng hoàn thành trong khối keyframe 20%, 50% hoặc 80% thay vì dùng keyframe to hoặc 100% để đảm bảo phần tử hoàn thành animate trong khi vẫn còn trong view.
Với view progress timeline, bạn có thể điều chỉnh phạm vi hiển thị của view progress.
Dùng view-timeline-inset, một phần của shorthand view-timeline, để điều chỉnh khi nào subject được coi là trong view. Giá trị mặc định là auto. Tác động của bất kỳ giá trị inset không phải auto nào giống như bạn đã di chuyển các cạnh của scroll port: giá trị dương tạo ra điều chỉnh vào trong, và giá trị âm tạo ra điều chỉnh ra ngoài.
Tương tự như scroll progress timeline, view progress timeline có thể được đặt tên hoặc ẩn danh.
Named view progress timeline
Named view progress timeline là timeline trong đó subject được đặt tên tường minh bằng thuộc tính view-timeline-name, một thành phần của shorthand view-timeline. Tên <dashed-ident> sau đó được liên kết với phần tử cần animate bằng cách chỉ định nó làm giá trị của thuộc tính animation-timeline của phần tử đó.
Với named view progress timeline, phần tử cần animate không cần phải là subject. Nói cách khác, phần tử kiểm soát timeline không cần phải là phần tử đang được animate. Điều này có nghĩa là bạn có thể animate một phần tử dựa trên chuyển động của phần tử khác trong scroll container có thể cuộn của nó.
Ở đây chúng ta dùng thuộc tính view-timeline-name để đặt tên cho một phần tử, xác định bản thân phần tử đó là nguồn của view progress timeline. Sau đó chúng ta đặt tên đó làm giá trị của thuộc tính animation-timeline.
.item {
animation: action 1ms linear;
view-timeline-name: --a-name;
animation-timeline: --a-name;
}
Chúng ta áp dụng animation trước animation timeline, vì animation đặt lại animation-timeline thành auto.
Animation hơi khác so với các ví dụ trước ở chỗ hiệu ứng xoay bắt đầu ở 20% và kết thúc ở 80% của quá trình animation; điều này có nghĩa là phần tử sẽ không xoay tích cực khi lần đầu tiên vào view và sẽ ngừng xoay trước khi hoàn toàn ra khỏi view.
@keyframes action {
0%,
20% {
rotate: 45deg;
}
80%,
100% {
rotate: 720deg;
}
}
Cuộn phần tử vào view. Lưu ý rằng phần tử animate qua animation @keyframes khi nó di chuyển qua vùng hiển thị của scroller tổ tiên của nó.
Anonymous view progress timeline: hàm view()
Ngoài ra, hàm view() có thể được đặt làm giá trị của thuộc tính animation-timeline để chỉ định rằng animation timeline của phần tử là anonymous view progress timeline. Điều này làm cho phần tử được animate dựa trên vị trí của nó bên trong scroller cha gần nhất.
Hàm view() tạo ra view timeline. Bạn gắn timeline vào phần tử muốn animate bằng thuộc tính animation-timeline. Hàm tạo ra view timeline cho mỗi phần tử khớp với selector.
Trong ví dụ này, chúng ta lại định nghĩa animation trước animation-timeline, để timeline không bị đặt lại. Sau đó chúng ta bao gồm hàm view() không có đối số. Chúng ta không chỉ định scroller, vì theo định nghĩa, khả năng hiển thị của subject được theo dõi bởi scroller tổ tiên gần nhất của nó.
.item {
animation: action 1ms linear;
animation-timeline: view();
}
Tham số của hàm view()
Hàm view() nhận tối đa ba giá trị tùy chọn làm đối số:
- Không hoặc một tham số
<axis>. Nếu được đặt, điều này chỉ định trục cuộn mà animation tiến triển theo. - Từ khóa
autohoặc không, một hoặc hai giá trị inset<length-percentage>. Nếu được đặt, các giá trị này chỉ định offset cho cạnh bắt đầu và/hoặc cạnh kết thúc của scrollport.
Khai báo view() tương đương với view(block auto), định nghĩa block là trục của phần tử cha cung cấp timeline và scroll-padding, thường mặc định là 0, làm inset trong vùng hiển thị mà animation bắt đầu và kết thúc.
Hàm đặt các giá trị của các thuộc tính view-timeline-axis và view-timeline-inset.
Các đối số view-timeline-inset chỉ định inset (nếu dương) hoặc outset (nếu âm) điều chỉnh điểm bắt đầu và kết thúc của scrollport. Chúng được dùng để xác định vị trí cuộn mà tại đó phần tử được coi là "trong view", xác định độ dài của animation timeline. Nói cách khác, thay vì bắt đầu ở cạnh bắt đầu và kết thúc ở cạnh cuối của scrollport, animation xảy ra ở đầu và cuối của view đã được điều chỉnh inset.
Khác với hàm scroll() của scroll timeline, không có đối số <scroller> trong hàm view(), vì view timeline luôn theo dõi subject trong scroll container tổ tiên gần nhất của nó.
Trong ví dụ này, vì chúng ta đang dùng giá trị inset, chúng ta có thể dùng các keyframe selector from và to.
@keyframes action {
from {
rotate: 45deg;
}
to {
rotate: 720deg;
}
}
.item {
animation: action 1ms linear;
animation-timeline: view(block 20% 20%);
}
Vấn đề về khả năng tiếp cận
Cũng như tất cả animation và transition, luôn tính đến tùy chọn prefers-reduced-motion của mọi người dùng.
Xóa timeline của animation
Đặt animation-timeline: none ngắt kết nối phần tử khỏi tất cả animation timeline, bao gồm cả timeline tài liệu dựa trên thời gian mặc định, nghĩa là phần tử sẽ không animate. Mặc dù một số animation có thể cần thiết, bạn có thể xóa animation dựa trên cài đặt prefers-reduced-motion của người dùng bằng:
@media (prefers-reduced-motion: reduce) {
.optionalAnimations {
animation-timeline: none;
}
}
Vì shorthand animation đặt animation-timeline thành auto, hãy dùng selector có đủ độ ưu tiên để đảm bảo animation-timeline của bạn không bị ghi đè bởi các khai báo shorthand animation.