Khi người dùng cuộn vùng chứa cuộn hai chiều, trình duyệt thường áp dụng phương pháp phỏng đoán khoá trục cuộn (còn gọi là "railing"). Nếu một cử chỉ bắt đầu bằng chuyển động dọc theo một trục nhiều hơn đáng kể so với trục vuông góc, thì trình duyệt sẽ khoá thao tác cuộn theo trục ngang hoặc trục dọc duy nhất đó và bỏ qua các delta vuông góc – thường là trong phần còn lại của cử chỉ.
Mặc dù tính năng khoá trục giúp ngăn chặn việc vô tình trượt sang một bên trong luồng đọc một chiều, nhưng tính năng này lại gây trở ngại cho các bề mặt kéo hai chiều, chẳng hạn như bản đồ tương tác, hình ảnh được phóng to, canvas vô hạn, biểu đồ nút và lưới dữ liệu lớn. Vì khoá vẫn tồn tại trong suốt cử chỉ, nên khoá này ảnh hưởng đến mọi thao tác kéo theo hai hướng – cho dù cả hai trục được kéo đồng thời (kéo theo đường chéo) hay lần lượt trong một cử chỉ (chẳng hạn như đường cong, đường tròn hoặc đường hình chữ L). Thuộc tính CSS scroll-axis-lock trực tiếp tắt phương pháp phỏng đoán này trên vùng chứa cuộn gốc, cho phép kéo không bị hạn chế theo hai chiều trong khi vẫn giữ nguyên thao tác cuộn luồng trình kết hợp, vật lý động lượng, thanh cuộn gốc và thao tác điều hướng bằng bàn phím.
Triển khai
1. Tắt tính năng khoá trục và chứa thao tác cuộn quá mức trên vùng chứa cuộn hai chiều
Áp dụng scroll-axis-lock: none trực tiếp cho phần tử thiết lập vùng chứa cuộn hai chiều (overflow: auto hoặc overflow: scroll). Mặc dù vùng chứa cuộn 2D đã có thể cập nhật độ lệch cuộn dọc theo cả hai trục theo giá trị auto mặc định, nhưng auto cho phép phương pháp phỏng đoán thanh chắn của trình duyệt bắt đầu khi một cử chỉ bắt đầu chủ yếu dọc theo một trục. Chế độ cài đặt scroll-axis-lock: none ngăn thanh ray đó tương tác trên màn hình cảm ứng, bàn di chuột và con lăn chuột.
Ghép nối scroll-axis-lock: none với overscroll-behavior: none (hoặc overscroll-behavior: contain) trên vùng chứa cuộn để khi đến một cạnh, thao tác cuộn không liên kết với trang mẹ hoặc kích hoạt thao tác điều hướng cuộn quá mức ngoài ý muốn của trình duyệt (chẳng hạn như thao tác vuốt ngang để quay lại/tiến lên hoặc kéo để làm mới).
2. Duy trì khả năng lấy tiêu điểm bàn phím và hỗ trợ tiếp cận
Vì bề mặt kéo 2D thường thiếu các thành phần điều khiển con có thể làm tâm điểm (ví dụ: bản đồ hoặc hình ảnh được thu phóng), hãy đảm bảo người dùng bàn phím có thể làm tâm điểm vùng chứa và kéo vùng chứa bằng các phím Mũi tên bằng cách thêm tabindex="0", role thích hợp và nhãn hỗ trợ tiếp cận, cũng như chỉ báo :focus-visible hiển thị.
Mã ví dụ
<!-- Make the 2D scroll container keyboard-focusable and labeled so keyboard
users can focus the viewport and pan in all directions with Arrow keys. -->
<div class="pan-viewport" tabindex="0" role="region" aria-label="Interactive canvas">
<div class="pan-surface">
<!-- 2D overflowing content (map tiles, zoomed image, canvas nodes, or grid) -->
</div>
</div>
.pan-viewport {
/* Example viewport dimensions; adjust to fit your layout. */
inline-size: 100%;
block-size: 500px;
/* MANDATORY: Establish a scroll container that can overflow on both axes. */
overflow: auto;
/* MANDATORY: Prevent browser scroll axis-locking ("railing") from kicking in
so 2D gestures never get locked to a single axis. */
scroll-axis-lock: none;
/* Contain scrolling and prevent accidental swipe navigations at edges
(use `none` or `contain`). */
overscroll-behavior: none;
}
/* Provide a clear focus indicator when keyboard users focus the viewport. */
.pan-viewport:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
.pan-surface {
/* Example surface dimensions larger than .pan-viewport in both axes. */
inline-size: 3000px;
block-size: 3000px;
}
Các phương pháp hay nhất và quy tắc ràng buộc
- Áp dụng thuộc tính này cho chính vùng chứa cuộn, chứ không phải trình bao bọc cấp trên:
scroll-axis-lockkhông được kế thừa, vì vậy, việc đặt thuộc tính này trên trình bao bọc sẽ không ảnh hưởng đến các vùng chứa cuộn cấp dưới. - Chứa hiệu ứng cuộn quá mức và ngăn chặn thao tác điều hướng ngoài ý muốn: Thêm
overscroll-behavior: nonehoặcoverscroll-behavior: containvào vùng chứa cuộn 2D để thao tác kéo vào ranh giới không liên kết với trang mẹ hoặc kích hoạt thao tác vuốt tiến/lùi và cử chỉ kéo để làm mới của trình duyệt. - Các bảng điều khiển một chiều lồng nhau sẽ tự động giữ lại tính năng khoá hướng: Vì
scroll-axis-lockkhông kế thừa, nên thanh bên hoặc bảng điều khiển lồng bên trong.pan-viewportsẽ giữ lại hành viscroll-axis-lock: automặc định. - Giữ
autotrên các nguồn cấp dữ liệu dọc, bài viết và danh sách một chiều tiêu chuẩn: Tính năng khoá trục giúp ngăn chặn tình trạng trôi dọc không mong muốn trong nội dung một chiều. - Không đặt
touch-action: nonevô điều kiện:touch-actionkiểm soát việc trình duyệt có xử lý cử chỉ chạm hay không (và không ảnh hưởng đến hoạt động nhập bằng bàn di chuột hoặc bánh xe), vì vậy, việc đặt thuộc tính này sẽ tắt tính năng cuộn bằng thao tác chạm của trình kết hợp gốc. Chỉ áp dụng trong@supports not (scroll-axis-lock: none)khi triển khai tính năng dự phòng con trỏ JavaScript. - Xoay bằng
scrollLeft/scrollTop, không bao giờ dùng CSStransform: Việc dịch chuyển bề mặt bằngtransformsẽ khiến phần tử ngừng hoạt động như một vùng chứa cuộn, làm hỏng thanh cuộn gốc, kẹp ranh giới, cuộn bằng phím mũi tên trên bàn phím, sự kiệnscroll,IntersectionObservervà ảnh động dựa trên thao tác cuộn.
Chiến lược dự phòng
Chọn giữa cải tiến tăng dần và dự phòng JavaScript dựa trên yêu cầu của ứng dụng:
Lựa chọn 1: Cải tiến tăng dần (Không có JavaScript)
Sử dụng chiến lược cải tiến tăng dần khi việc duy trì thao tác cuộn bằng cách chạm, động lượng vuốt theo quán tính và chụm để thu phóng của luồng trình kết hợp gốc quan trọng hơn việc loại bỏ tính năng khoá trục trong các trình duyệt không được hỗ trợ:
- Trình duyệt hỗ trợ: Trình duyệt ngăn không cho phương pháp phỏng đoán khoá trục ("railing") kích hoạt trên vùng chứa cuộn 2D trong các cử chỉ cuộn.
- Trình duyệt không được hỗ trợ: Trình duyệt không nhận dạng được
scroll-axis-lock: nonesẽ bỏ qua nội dung khai báo một cách an toàn và giữ nguyên hành vi cuộn 2D mặc định củaauto– cuộn trên cả hai trục theo cách tự nhiên, trong khi vẫn giữ nguyên các cử chỉ bắt đầu chủ yếu dọc theo một trục.
Lựa chọn 2: Dự phòng JavaScript (wheel + pointermove với scrollLeft/scrollTop)
Nếu bạn cần tính năng kéo hai chiều không bị hạn chế trên tất cả trình duyệt, hãy sử dụng @supports not (scroll-axis-lock: none) và CSS.supports('scroll-axis-lock', 'none') để chặn có điều kiện các cử chỉ kéo con trỏ và wheel.
Vì wheel (e.deltaX, e.deltaY) và pointermove (e.clientX, e.clientY) hiển thị các delta 2D thô, chưa khoá trước khi công cụ cuộn của trình duyệt áp dụng tính năng khoá trục, nên bạn có thể áp dụng trực tiếp các delta đó cho scrollLeft và scrollTop:
- Tại sao nên dùng
scrollLeft/scrollTopthay vì CSStransform: Việc cập nhậtscrollLeftvàscrollTopgiúp phần tử giữ nguyên vai trò là một vùng chứa cuộn thực sự. Trình duyệt tự động kẹp các vị trí cuộn vào[0, scrollWidth - clientWidth]và[0, scrollHeight - clientHeight], thanh cuộn gốc vẫn được đồng bộ hoá, thao tác cuộn bằng phím mũi tên trên bàn phím tiếp tục hoạt động, còn các sự kiệnscrollvàIntersectionObserverkích hoạt bình thường. - Sự đánh đổi: Việc chặn
wheelbằng{ passive: false }sẽ di chuyển thao tác cuộn sang luồng chính và việc đặttouch-action: nonetrên màn hình cảm ứng sẽ tắt động lượng vuốt quán tính gốc saupointerup(cũng như tính năng chụm để thu phóng gốc).
/* Only disable native touch scrolling when scroll-axis-lock is unsupported
and the JavaScript pointer fallback below is active. */
@supports not (scroll-axis-lock: none) {
.pan-viewport {
touch-action: none;
}
}
// Only attach the main-thread gesture fallback when native support is missing
if (!CSS.supports('scroll-axis-lock', 'none')) {
const viewport = document.querySelector('.pan-viewport');
let lastPointer = null;
// 1. Trackpad and mouse wheel fallback
viewport.addEventListener(
'wheel',
(e) => {
e.preventDefault();
viewport.scrollLeft += e.deltaX;
viewport.scrollTop += e.deltaY;
},
{ passive: false },
);
// 2. Touch and pointer drag fallback
viewport.addEventListener('pointerdown', (e) => {
// Ignore non-primary buttons and clicks on native scrollbars
const rect = viewport.getBoundingClientRect();
if (
!e.isPrimary ||
e.button !== 0 ||
e.clientX - rect.left - viewport.clientLeft >= viewport.clientWidth ||
e.clientY - rect.top - viewport.clientTop >= viewport.clientHeight
) {
return;
}
lastPointer = { x: e.clientX, y: e.clientY };
viewport.setPointerCapture(e.pointerId);
});
viewport.addEventListener('pointermove', (e) => {
if (!lastPointer) return;
viewport.scrollLeft -= e.clientX - lastPointer.x;
viewport.scrollTop -= e.clientY - lastPointer.y;
lastPointer = { x: e.clientX, y: e.clientY };
});
const endPointerPan = () => {
lastPointer = null;
};
viewport.addEventListener('pointerup', endPointerPan);
viewport.addEventListener('pointercancel', endPointerPan);
}