เมื่อผู้ใช้เลื่อนคอนเทนเนอร์การเลื่อนแบบ 2 มิติ โดยปกติแล้วเบราว์เซอร์จะใช้การคาดคะเนการล็อกแกนการเลื่อน (หรือที่เรียกว่า "การเลื่อนตามแนวแกน") หากท่าทางสัมผัสเริ่มต้นด้วยการเคลื่อนไหวตามแกนหนึ่งมากกว่าแกนที่ตั้งฉากอย่างมีนัยสำคัญ เบราว์เซอร์จะล็อกการเลื่อนไปยังแกนแนวนอนหรือแนวตั้งแกนเดียว และละเว้นเดลต้าที่ตั้งฉาก ซึ่งมักจะเป็นส่วนที่เหลือของท่าทางสัมผัส
แม้ว่าการล็อกแกนจะช่วยป้องกันการเลื่อนไปด้านข้างโดยไม่ตั้งใจในขั้นตอนการอ่านแบบมิติเดียว แต่ก็ทำให้เกิดความขัดแย้งในพื้นผิวการเลื่อนแบบ 2 มิติ เช่น แผนที่แบบอินเทอร์แอกทีฟ รูปภาพที่ซูม แคนวาสแบบไม่จำกัด กราฟโหนด และตารางกริดข้อมูลขนาดใหญ่ เนื่องจากการล็อกจะคงอยู่ตลอดท่าทางสัมผัส จึงส่งผลต่อการเลื่อนที่เกี่ยวข้องกับ 2 ทิศทาง ไม่ว่าจะเป็นการเลื่อนทั้ง 2 แกนพร้อมกัน (การเลื่อนแนวทแยง) หรือเลื่อนทีละแกนภายในท่าทางสัมผัสเดียว (เช่น เส้นทางโค้ง วงกลม หรือรูปตัว L) พร็อพเพอร์ตี้ CSS scroll-axis-lock จะปิดใช้การเรียนรู้จากประสบการณ์นี้โดยตรงในคอนเทนเนอร์การเลื่อนแบบเนทีฟ ซึ่งช่วยให้เลื่อนแบบ 2 มิติได้อย่างไม่จำกัด ขณะเดียวกันก็ยังคงรักษาการเลื่อนเทรดคอมโพสิเตอร์ ฟิสิกส์โมเมนตัม แถบเลื่อนแบบเนทีฟ และการไปยังส่วนต่างๆ ด้วยแป้นพิมพ์ไว้
การใช้งาน
1. ปิดใช้การล็อกแกนและจำกัดการเลื่อนเกินในคอนเทนเนอร์การเลื่อนแบบ 2 มิติ
ใช้ scroll-axis-lock: none กับองค์ประกอบที่สร้างคอนเทนเนอร์การเลื่อนแบบ 2 มิติ (overflow: auto หรือ overflow: scroll) โดยตรง แม้ว่าคอนเทนเนอร์การเลื่อนแบบ 2 มิติจะอัปเดตเดลต้าการเลื่อนตามแกนทั้ง 2 แกนภายใต้ค่า auto เริ่มต้นได้อยู่แล้ว แต่ auto จะช่วยให้การวิเคราะห์พฤติกรรมของราวของเบราว์เซอร์เริ่มทำงานเมื่อท่าทางสัมผัสเริ่มขึ้นตามแกนใดแกนหนึ่งเป็นหลัก การตั้งค่า scroll-axis-lock: none จะป้องกันไม่ให้แถบเลื่อนดังกล่าวทำงานในหน้าจอสัมผัส แทร็กแพด และลูกกลิ้งเมาส์
จับคู่ scroll-axis-lock: none กับ overscroll-behavior: none (หรือ overscroll-behavior: contain) ในคอนเทนเนอร์การเลื่อนเพื่อให้การเลื่อนไปถึงขอบไม่เชื่อมโยงการเลื่อนไปยังหน้าหลักหรือทริกเกอร์การไปยังส่วนต่างๆ ของเบราว์เซอร์ที่เลื่อนเกินโดยไม่ตั้งใจ (เช่น การไปยังส่วนต่างๆ ด้วยการปัดไปข้างหลัง/ข้างหน้าในแนวนอนหรือการดึงเพื่อรีเฟรช)
2. คงความสามารถในการโฟกัสของแป้นพิมพ์และการช่วยเหลือพิเศษไว้
เนื่องจากพื้นผิวการแพนแบบ 2 มิติมักไม่มีการควบคุมย่อยที่โฟกัสได้ (เช่น แผนที่หรือรูปภาพที่ซูม) โปรดตรวจสอบว่าผู้ใช้แป้นพิมพ์สามารถโฟกัสคอนเทนเนอร์และแพนด้วยแป้นลูกศรได้โดยเพิ่ม tabindex="0", role ที่เหมาะสมและป้ายกำกับที่เข้าถึงได้ รวมถึงตัวบ่งชี้ :focus-visible ที่มองเห็นได้
โค้ดตัวอย่าง
<!-- 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;
}
แนวทางปฏิบัติแนะนำและข้อจำกัด
- ใช้กับคอนเทนเนอร์การเลื่อนเอง ไม่ใช่ Wrapper บรรพบุรุษ:
scroll-axis-lockไม่ได้รับการรับช่วง ดังนั้นการตั้งค่าใน Wrapper จึงไม่มีผลกับคอนเทนเนอร์การเลื่อนสืบทอด - จำกัดการเลื่อนเกินและป้องกันการไปยังส่วนต่างๆ โดยไม่ตั้งใจ: เพิ่ม
overscroll-behavior: noneหรือoverscroll-behavior: containลงในคอนเทนเนอร์การเลื่อน 2 มิติ เพื่อไม่ให้การเลื่อนเข้าไปในขอบเขตเชื่อมโยงกับหน้าหลักหรือทริกเกอร์การไปยังส่วนต่างๆ ด้วยการปัดไปข้างหลัง/ข้างหน้าของเบราว์เซอร์และท่าทางสัมผัสดึงเพื่อรีเฟรช - แผงแบบมิติเดียวที่ฝังอยู่จะยังคงใช้การจัดวางแบบรางโดยอัตโนมัติ: เนื่องจาก
scroll-axis-lockไม่รับค่า แถบด้านข้างหรือแผงเครื่องมือตรวจสอบที่ฝังอยู่ภายใน.pan-viewportจึงยังคงใช้ลักษณะการทำงานเริ่มต้นของscroll-axis-lock: auto - ใช้
autoกับฟีดแนวตั้ง บทความ และรายการแบบมิติเดียวมาตรฐาน: การล็อกแกนจะป้องกันการเลื่อนในแนวตั้งฉากโดยไม่ตั้งใจในเนื้อหาแบบทิศทางเดียว - อย่าตั้งค่า
touch-action: noneโดยไม่มีเงื่อนไข:touch-actionควบคุมว่าเบราว์เซอร์จะจัดการท่าทางสัมผัสหรือไม่ (และไม่มีผลต่อการป้อนข้อมูลด้วยแทร็กแพดหรือล้อเลื่อน) ดังนั้นการตั้งค่านี้จะปิดใช้การเลื่อนด้วยการสัมผัสของคอมโพสิเตอร์แบบเนทีฟ ใช้เฉพาะภายใน@supports not (scroll-axis-lock: none)เมื่อใช้ตัวชี้ JavaScript สำรอง - เลื่อนด้วย
scrollLeft/scrollTopไม่ใช้ CSStransform: การแปลพื้นผิวด้วยtransformจะทำให้องค์ประกอบไม่ทำงานเป็นคอนเทนเนอร์การเลื่อน ซึ่งจะทำให้แถบเลื่อนแบบเนทีฟ, การจำกัดขอบเขต, การเลื่อนด้วยแป้นลูกศรบนแป้นพิมพ์, เหตุการณ์scroll,IntersectionObserverและภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อนใช้งานไม่ได้
กลยุทธ์สำรอง
เลือกระหว่างการเพิ่มประสิทธิภาพแบบต่อเนื่องและทางเลือกสำรองของ JavaScript โดยอิงตามข้อกำหนดของแอปพลิเคชัน ดังนี้
ตัวเลือกที่ 1: การเพิ่มประสิทธิภาพแบบต่อเนื่อง (ไม่มี JavaScript)
ใช้การเพิ่มประสิทธิภาพแบบต่อเนื่องเมื่อการรักษาการเลื่อนด้วยการสัมผัสในเทรด Compositor แบบเนทีฟ โมเมนตัมการเหวี่ยงแบบเฉื่อย และการบีบเพื่อซูมมีความสำคัญมากกว่าการกำจัดการล็อกแกนในเบราว์เซอร์ที่ไม่รองรับ ดังนี้
- เบราว์เซอร์ที่รองรับ: เบราว์เซอร์จะป้องกันไม่ให้การเรียนรู้ของระบบการล็อกแกน ("การจำกัด") เริ่มทำงานในคอนเทนเนอร์การเลื่อน 2 มิติระหว่างท่าทางการเลื่อน
- เบราว์เซอร์ที่ไม่รองรับ: เบราว์เซอร์ที่ไม่รู้จัก
scroll-axis-lock: noneจะละเว้นการประกาศอย่างปลอดภัยและคงลักษณะการเลื่อน 2 มิติautoเริ่มต้นไว้ ซึ่งก็คือการเลื่อนทั้ง 2 แกนโดยค่าเริ่มต้น ขณะที่ยังคงจำกัดท่าทางสัมผัสที่เริ่มตามแกนเดียวเป็นหลัก
ตัวเลือกที่ 2: JavaScript สำรอง (wheel + pointermove พร้อม scrollLeft/scrollTop)
หากต้องการการเลื่อนแบบ 2 มิติที่ไม่มีข้อจำกัดในเบราว์เซอร์ทั้งหมด ให้ใช้ @supports not (scroll-axis-lock: none) และ CSS.supports('scroll-axis-lock', 'none') เพื่อสกัดกั้น wheel และท่าทางสัมผัสการลากเคอร์เซอร์ตามเงื่อนไข
เนื่องจาก wheel (e.deltaX, e.deltaY) และ pointermove (e.clientX, e.clientY) แสดงเดลต้า 2 มิติแบบดิบที่ไม่ได้ล็อกก่อนที่เครื่องมือการเลื่อนของเบราว์เซอร์จะใช้การล็อกแกน คุณจึงใช้เดลต้าเหล่านั้นกับ scrollLeft และ scrollTop ได้โดยตรง ดังนี้
- เหตุผลที่ควรใช้
scrollLeft/scrollTopแทน CSStransform: การอัปเดตscrollLeftและscrollTopจะทำให้องค์ประกอบยังคงเป็นคอนเทนเนอร์การเลื่อนที่แท้จริง เบราว์เซอร์จะจำกัดตำแหน่งการเลื่อนไว้ที่[0, scrollWidth - clientWidth]และ[0, scrollHeight - clientHeight]โดยอัตโนมัติ แถบเลื่อนแบบเนทีฟจะยังคงซิงค์กัน การเลื่อนด้วยปุ่มลูกศรบนแป้นพิมพ์จะยังคงทำงานต่อไป และเหตุการณ์scrollและIntersectionObserverจะเริ่มทำงานตามปกติ - ข้อเสีย: การสกัดกั้น
wheelด้วย{ passive: false }จะย้ายการเลื่อนไปยังเทรดหลัก และการตั้งค่าtouch-action: noneในหน้าจอสัมผัสจะปิดใช้โมเมนตัมการเหวี่ยงตามแรงเฉื่อยแบบเนทีฟหลังจากpointerup(รวมถึงการบีบเพื่อซูมแบบเนทีฟ)
/* 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);
}