Thanh cuộn tuỳ chỉnh rất hiếm khi xuất hiện và điều này chủ yếu là do thanh cuộn là một trong những phần còn lại trên web mà hầu như không thể tạo kiểu (tôi đang nói đến bạn, bộ chọn ngày). Bạn có thể sử dụng JavaScript để tạo riêng, nhưng cách này tốn kém, độ trung thực thấp và có thể bị giật. Trong bài viết này, chúng ta sẽ tận dụng một số ma trận CSS không thông thường để tạo một thanh cuộn tuỳ chỉnh không yêu cầu JavaScript nào trong khi cuộn, chỉ cần một số mã thiết lập.
TL;DR
Bạn không quan tâm đến những điều nhỏ nhặt? Bạn chỉ muốn xem bản minh hoạ về mèo Nyan và tải thư viện xuống? Bạn có thể tìm thấy mã của bản minh hoạ trong kho lưu trữ GitHub của chúng tôi.
LAM;WRA (Dài và mang tính toán học; sẽ đọc dù sao đi nữa)
Cách đây không lâu, chúng ta đã tạo một thanh cuộn có hiệu ứng thị giác (Bạn đã đọc bài viết đó chưa? Ứng dụng này rất hay, đáng để bạn dành thời gian tìm hiểu. Bằng cách đẩy các phần tử trở lại bằng phép biến đổi CSS 3D, các phần tử di chuyển chậm hơn so với tốc độ cuộn thực tế của chúng ta.
Tóm tắt
Hãy bắt đầu bằng cách tóm tắt cách hoạt động của thanh cuộn hiệu ứng thị sai.
Như trong ảnh động, chúng ta đã đạt được hiệu ứng thị sai bằng cách đẩy các phần tử "lùi" trong không gian 3D, dọc theo trục Z. Việc cuộn tài liệu thực sự là một bản dịch dọc theo trục Y. Vì vậy, nếu chúng ta cuộn xuống, giả sử là 100px, thì mọi phần tử sẽ được dịch lên 100px. Điều đó áp dụng cho tất cả phần tử, ngay cả những phần tử "ở xa hơn". Nhưng vì các phần tử này ở xa máy ảnh hơn, nên chuyển động được quan sát trên màn hình sẽ nhỏ hơn 100px, tạo ra hiệu ứng thị sai mong muốn.
Tất nhiên, việc di chuyển một phần tử trở lại không gian cũng sẽ khiến phần tử đó trông nhỏ hơn. Chúng ta sẽ khắc phục điều này bằng cách điều chỉnh tỷ lệ phần tử đó trở lại. Chúng ta đã tìm ra toán học chính xác khi tạo trình cuộn hiệu ứng thị sai, vì vậy, tôi sẽ không lặp lại tất cả thông tin chi tiết.
Bước 0: Chúng ta muốn làm gì?
Thanh cuộn. Đó là những gì chúng ta sẽ xây dựng. Nhưng bạn đã bao giờ thực sự nghĩ về những gì họ làm chưa? Tôi chắc chắn là không. Thanh cuộn là chỉ báo cho biết mức độ nội dung hiện có đang hiển thị và mức độ tiến trình của bạn khi đọc. Nếu bạn cuộn xuống, thanh cuộn cũng sẽ cuộn xuống để cho biết bạn đang tiến đến cuối. Nếu tất cả nội dung đều vừa với khung nhìn, thanh cuộn thường bị ẩn. Nếu nội dung có chiều cao gấp đôi chiều cao của khung nhìn, thì thanh cuộn sẽ lấp đầy ½ chiều cao của khung nhìn. Nội dung có chiều cao gấp 3 lần chiều cao của khung nhìn sẽ điều chỉnh thanh cuộn thành ⅓ của khung nhìn, v.v. Bạn sẽ thấy mẫu này. Thay vì cuộn, bạn cũng có thể nhấp và kéo thanh cuộn để di chuyển nhanh hơn trên trang web. Đó là một lượng hành vi đáng ngạc nhiên đối với một phần tử không nổi bật như vậy. Hãy cùng chiến đấu từng trận một.
Bước 1: Đặt chế độ đảo ngược
Được rồi, chúng ta có thể làm cho các phần tử di chuyển chậm hơn tốc độ cuộn bằng các phép biến đổi CSS 3D như đã nêu trong bài viết về hiệu ứng cuộn theo hiệu ứng thị giác. Chúng ta cũng có thể đảo ngược hướng không? Hóa ra chúng ta có thể và đó là cách chúng ta tạo một thanh cuộn tuỳ chỉnh, hoàn hảo về khung. Để hiểu cách hoạt động của tính năng này, trước tiên, chúng ta cần tìm hiểu một số khái niệm cơ bản về CSS 3D.
Để có được bất kỳ loại hình chiếu phối cảnh nào theo nghĩa toán học, rất có thể bạn sẽ sử dụng toạ độ đồng nhất. Tôi sẽ không đi sâu vào chi tiết về những giá trị này và lý do chúng hoạt động, nhưng bạn có thể coi chúng như toạ độ 3D với một toạ độ thứ tư bổ sung có tên là w. Toạ độ này phải là 1, trừ phi bạn muốn có độ méo phối cảnh. Chúng ta không cần phải lo lắng về thông tin chi tiết của w vì chúng ta sẽ không sử dụng bất kỳ giá trị nào khác ngoài 1. Do đó, từ giờ trở đi, tất cả các điểm đều là vectơ 4 chiều [x, y, z, w=1] và do đó, ma trận cũng cần phải có kích thước 4x4.
Một trường hợp mà bạn có thể thấy CSS sử dụng toạ độ đồng nhất là khi bạn xác định ma trận 4x4 của riêng mình trong thuộc tính biến đổi bằng hàm matrix3d()
. matrix3d
nhận 16 đối số (vì ma trận là 4x4), chỉ định từng cột theo thứ tự. Vì vậy, chúng ta có thể sử dụng hàm này để chỉ định thủ công các phép xoay, dịch, v.v. Tuy nhiên, hàm này cũng cho phép chúng ta làm rối tung toà với toạ độ w đó!
Trước khi có thể sử dụng matrix3d()
, chúng ta cần có ngữ cảnh 3D – vì nếu không có ngữ cảnh 3D, sẽ không có bất kỳ sự méo hình phối cảnh nào và không cần đến toạ độ đồng nhất. Để tạo bối cảnh 3D, chúng ta cần một vùng chứa có perspective
và một số phần tử bên trong mà chúng ta có thể biến đổi trong không gian 3D mới tạo. Ví dụ:
ví dụ:
Các phần tử bên trong vùng chứa phối cảnh được công cụ CSS xử lý như sau:
- Chuyển đổi mỗi góc (đỉnh) của một phần tử thành toạ độ đồng nhất
[x,y,z,w]
, tương ứng với vùng chứa phối cảnh. - Áp dụng tất cả các phép biến đổi của phần tử dưới dạng ma trận từ phải sang trái.
- Nếu phần tử phối cảnh có thể cuộn, hãy áp dụng ma trận cuộn.
- Áp dụng ma trận phối cảnh.
Ma trận cuộn là một phép dịch dọc theo trục y. Nếu chúng ta cuộn xuống 400px, tất cả các phần tử cần được di chuyển lên 400px. Ma trận phối cảnh là một ma trận "kéo" các điểm lại gần điểm vanishing (điểm biến mất) hơn khi các điểm đó ở xa hơn trong không gian 3D. Điều này mang lại cả hai hiệu ứng là làm cho mọi thứ trông nhỏ hơn khi ở xa hơn và cũng khiến chúng "di chuyển chậm hơn" khi được dịch. Vì vậy, nếu một phần tử bị đẩy lùi, thì việc dịch 400px sẽ khiến phần tử đó chỉ di chuyển 300px trên màn hình.
Nếu muốn biết tất cả thông tin chi tiết, bạn nên đọc quy cách về mô hình kết xuất biến đổi của CSS. Tuy nhiên, để phù hợp với bài viết này, tôi đã đơn giản hoá thuật toán ở trên.
Hộp của chúng ta nằm bên trong vùng chứa phối cảnh có giá trị p cho thuộc tính perspective
. Giả sử vùng chứa này có thể cuộn và được cuộn xuống n pixel.
Ma trận đầu tiên là ma trận phối cảnh, ma trận thứ hai là ma trận cuộn. Tóm lại: Nhiệm vụ của ma trận cuộn là làm cho một phần tử di chuyển lên khi chúng ta cuộn xuống, do đó có dấu âm.
Tuy nhiên, đối với thanh cuộn, chúng ta muốn ngược lại – chúng ta muốn phần tử của mình di chuyển xuống khi chúng ta cuộn xuống. Đây là nơi chúng ta có thể sử dụng một mẹo: Đảo ngược toạ độ w của các góc hộp. Nếu toạ độ w là -1, thì tất cả các phép dịch sẽ có hiệu lực theo hướng ngược lại. Vậy chúng ta làm như thế nào? Công cụ CSS sẽ chuyển đổi các góc của hộp thành hệ toạ độ đồng nhất và đặt w thành 1. Đã đến lúc matrix3d()
tỏa sáng!
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
);
}
Ma trận này sẽ không làm gì khác ngoài việc phủ định w. Vì vậy, khi công cụ CSS đã chuyển đổi mỗi góc thành một vectơ có dạng [x,y,z,1]
, ma trận sẽ chuyển đổi vectơ đó thành [x,y,z,-1]
.
Tôi đã liệt kê một bước trung gian để cho thấy hiệu ứng của ma trận biến đổi phần tử. Nếu bạn không quen với toán học ma trận, thì điều đó không sao. Khoảnh khắc Eureka là ở dòng cuối cùng, chúng ta sẽ thêm độ dời cuộn n vào toạ độ y thay vì trừ đi độ dời đó. Phần tử sẽ được dịch xuống nếu chúng ta cuộn xuống.
Tuy nhiên, nếu chúng ta chỉ đặt ma trận này vào ví dụ, thì phần tử này sẽ không hiển thị. Điều này là do quy cách CSS yêu cầu mọi đỉnh có w < 0 sẽ chặn việc hiển thị phần tử. Và vì toạ độ z hiện là 0 và p là 1, nên w sẽ là -1.
May mắn thay, chúng ta có thể chọn giá trị của z! Để đảm bảo kết quả cuối cùng là w=1, chúng ta cần đặt z = -2.
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
translateZ(-2px);
}
Và thật bất ngờ, hộp của chúng ta đã trở lại!
Bước 2: Làm cho hình ảnh chuyển động
Bây giờ, hộp của chúng ta đã xuất hiện và trông giống như khi không có bất kỳ phép biến đổi nào. Hiện tại, vùng chứa phối cảnh không thể cuộn được nên chúng ta không thể thấy vùng chứa này, nhưng chúng ta biết rằng phần tử của chúng ta sẽ đi theo hướng khác khi cuộn. Vậy hãy cùng cuộn vùng chứa nhé. Chúng ta chỉ cần thêm một phần tử khoảng đệm chiếm không gian:
<div class="container">
<div class="box"></div>
<span class="spacer"></span>
</div>
<style>
/* … all the styles from the previous example … */
.container {
overflow: scroll;
}
.spacer {
display: block;
height: 500px;
}
</style>
Và giờ, hãy cuộn hộp! Hộp màu đỏ di chuyển xuống.
Bước 3: Đặt kích thước cho hình ảnh
Chúng ta có một phần tử di chuyển xuống khi trang cuộn xuống. Đó là phần khó nhất. Bây giờ, chúng ta cần tạo kiểu cho thanh này để trông giống như thanh cuộn và làm cho thanh này tương tác hơn một chút.
Thanh cuộn thường bao gồm một "con trỏ" và một "dải", trong khi dải không phải lúc nào cũng hiển thị. Chiều cao của hình thu nhỏ tỷ lệ thuận với lượng nội dung hiển thị.
<script>
const scroller = document.querySelector('.container');
const thumb = document.querySelector('.box');
const scrollerHeight = scroller.getBoundingClientRect().height;
thumb.style.height = /* ??? */;
</script>
scrollerHeight
là chiều cao của phần tử có thể cuộn, còn scroller.scrollHeight
là tổng chiều cao của nội dung có thể cuộn.
scrollerHeight/scroller.scrollHeight
là phần nội dung hiển thị. Tỷ lệ không gian dọc mà ngón tay cái che phải bằng tỷ lệ nội dung hiển thị:
<script>
// …
thumb.style.height =
scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
// Accommodate for native scrollbars
thumb.style.right =
(scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>
Kích thước của ngón tay cái trông ổn, nhưng nó di chuyển quá nhanh. Đây là nơi chúng ta có thể lấy kỹ thuật của mình từ thanh cuộn hiệu ứng thị sai. Nếu chúng ta di chuyển phần tử ra xa hơn, phần tử đó sẽ di chuyển chậm hơn trong khi cuộn. Chúng ta có thể điều chỉnh kích thước bằng cách tăng tỷ lệ. Nhưng chính xác thì chúng ta nên đẩy giá trị này trở lại bao nhiêu? Hãy cùng làm một số phép tính! Đây là lần cuối cùng, tôi hứa.
Thông tin quan trọng là chúng ta muốn cạnh dưới của ngón tay cái căn chỉnh với cạnh dưới của phần tử có thể cuộn khi cuộn xuống hết. Nói cách khác: Nếu đã cuộn scroller.scrollHeight - scroller.height
pixel, chúng ta muốn ngón tay cái được dịch bởi scroller.height - thumb.height
. Đối với mỗi pixel của thanh cuộn, chúng ta muốn ngón tay cái di chuyển một phần của pixel:
Đó là hệ số tỷ lệ của chúng ta. Bây giờ, chúng ta cần chuyển đổi hệ số tỷ lệ thành một phép dịch dọc theo trục z, như chúng ta đã làm trong bài viết về hiệu ứng cuộn song song. Theo phần có liên quan trong thông số kỹ thuật: Hệ số tỷ lệ bằng p/(p − z). Chúng ta có thể giải phương trình này cho z để tìm ra lượng cần dịch ngón tay cái dọc theo trục z. Tuy nhiên, hãy lưu ý rằng do các trò gian lận về toạ độ w, chúng ta cần dịch thêm một -2px
dọc theo z. Ngoài ra, hãy lưu ý rằng các phép biến đổi của một phần tử được áp dụng từ phải sang trái, nghĩa là tất cả các phép dịch trước ma trận đặc biệt của chúng ta sẽ không bị đảo ngược, tuy nhiên, tất cả các phép dịch sau ma trận đặc biệt của chúng ta sẽ bị đảo ngược! Hãy cùng mã hoá điều này!
<script>
// ... code from above...
const factor =
(scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
thumb.style.transform = `
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
scale(${1/factor})
translateZ(${1 - 1/factor}px)
translateZ(-2px)
`;
</script>
Chúng ta có một thanh cuộn! Và đó chỉ là một phần tử DOM mà chúng ta có thể tạo kiểu theo ý muốn. Một điều quan trọng cần làm về khả năng hỗ trợ tiếp cận là làm cho ngón tay cái phản hồi thao tác nhấp và kéo, vì nhiều người dùng đã quen tương tác với thanh cuộn theo cách đó. Để bài đăng trên blog này không dài hơn nữa, tôi sẽ không giải thích chi tiết về phần đó. Hãy xem mã thư viện để biết thông tin chi tiết nếu bạn muốn xem cách thực hiện.
Còn iOS thì sao?
Ôi, Safari trên iOS, người bạn cũ của tôi. Cũng như khi cuộn theo hiệu ứng thị sai, chúng ta gặp phải một vấn đề ở đây. Vì đang cuộn trên một phần tử, nên chúng ta cần chỉ định -webkit-overflow-scrolling: touch
, nhưng điều đó sẽ làm cho hiệu ứng 3D bị làm phẳng và toàn bộ hiệu ứng cuộn của chúng ta sẽ ngừng hoạt động. Chúng tôi đã giải quyết vấn đề này trong thanh cuộn hiệu ứng thị sai bằng cách phát hiện iOS Safari và dựa vào position: sticky
làm giải pháp. Chúng ta sẽ làm tương tự như vậy ở đây. Hãy xem bài viết về hiệu ứng thị sai để làm mới trí nhớ của bạn.
Còn thanh cuộn của trình duyệt thì sao?
Trên một số hệ thống, chúng ta sẽ phải xử lý thanh cuộn gốc, cố định.
Trước đây, bạn không thể ẩn thanh cuộn (ngoại trừ bộ chọn giả lập không chuẩn).
Vì vậy, để ẩn thông tin này, chúng ta phải sử dụng một số thủ thuật (không cần toán học). Chúng ta gói phần tử cuộn trong một vùng chứa có overflow-x: hidden
và làm cho phần tử cuộn rộng hơn vùng chứa. Thanh cuộn gốc của trình duyệt hiện không hiển thị.
Vây
Khi kết hợp tất cả các thành phần này, chúng ta có thể tạo một thanh cuộn tuỳ chỉnh hoàn hảo theo khung – giống như thanh cuộn trong bản minh hoạ về mèo Nyan.
Nếu không thấy mèo Nyan, bạn đang gặp phải lỗi mà chúng tôi đã phát hiện và báo cáo trong khi tạo bản minh hoạ này (hãy nhấp vào biểu tượng ngón tay cái để mèo Nyan xuất hiện). Chrome rất giỏi trong việc tránh những công việc không cần thiết như vẽ hoặc tạo ảnh động cho những nội dung nằm ngoài màn hình. Tin xấu là những trò gian lận về ma trận của chúng ta khiến Chrome nghĩ rằng ảnh gif con mèo Nyan thực sự nằm ngoài màn hình. Hy vọng vấn đề này sẽ sớm được khắc phục.
Vậy là xong. Đó là một công việc rất lớn. Tôi rất cảm ơn bạn đã đọc toàn bộ nội dung này. Đây là một số thủ thuật thực sự để làm cho thanh cuộn này hoạt động và có thể hiếm khi đáng để nỗ lực, ngoại trừ khi thanh cuộn tuỳ chỉnh là một phần thiết yếu của trải nghiệm. Nhưng thật vui khi biết rằng điều này là có thể, phải không? Việc khó tạo thanh cuộn tuỳ chỉnh cho thấy rằng bạn cần phải làm việc ở phía CSS. Nhưng đừng lo! Trong tương lai, AnimationWorklet của Houdini sẽ giúp bạn tạo hiệu ứng cuộn liên kết hoàn hảo như thế này dễ dàng hơn nhiều.