Wenn ein Nutzer in einem zweidimensionalen Scroll-Container scrollt, wenden Browser in der Regel eine Heuristik zur Sperrung der Scrollachse an (auch als „Railing“ bezeichnet). Wenn eine Geste mit deutlich mehr Bewegung entlang einer Achse als entlang der senkrechten Achse beginnt, sperrt der Browser das Scrollen auf diese einzelne horizontale oder vertikale Achse und ignoriert senkrechte Deltas – oft für den Rest der Geste.
Die Achsensperre verhindert zwar ein versehentliches seitliches Abdriften in eindimensionalen Leseflüssen, führt aber zu Reibung auf zweidimensionalen Schwenkflächen wie interaktiven Karten, gezoomten Bildern, unendlichen Leinwänden, Knotengrafiken und großen Datenrastern. Da die Sperre während der gesamten Geste aktiv bleibt, wirkt sie sich auf alle Schwenkbewegungen aus, die in zwei Richtungen erfolgen – unabhängig davon, ob beide Achsen gleichzeitig (diagonales Schwenken) oder nacheinander innerhalb einer einzelnen Geste (z. B. gekrümmte, kreisförmige oder L-förmige Pfade) geschwenkt werden. Mit der CSS-Property scroll-axis-lock wird diese Heuristik direkt in einem nativen Scrollcontainer deaktiviert. Dadurch ist uneingeschränktes zweidimensionales Schwenken möglich, während das Scrollen im Compositor-Thread, die Momentum-Physik, native Scrollleisten und die Tastaturnavigation beibehalten werden.
Implementierung
1. Achsensperre deaktivieren und Overscroll im zweidimensionalen Scrollcontainer einschränken
Wende scroll-axis-lock: none direkt auf das Element an, das den zweidimensionalen Scrollcontainer (overflow: auto oder overflow: scroll) einrichtet. Ein 2D-Scrollcontainer kann zwar bereits mit dem Standardwert auto Scroll-Deltas entlang beider Achsen aktualisieren, aber mit auto kann die Railing-Heuristik des Browsers aktiviert werden, wenn eine Geste hauptsächlich entlang einer Achse beginnt. Wenn Sie scroll-axis-lock: none festlegen, wird verhindert, dass diese Begrenzung auf Touchscreens, Trackpads und Mausrädern aktiviert wird.
Kombinieren Sie scroll-axis-lock: none mit overscroll-behavior: none (oder overscroll-behavior: contain) im Scroll-Container, damit das Erreichen eines Rands nicht dazu führt, dass das Scrollen auf der übergeordneten Seite fortgesetzt wird oder versehentliche Overscroll-Navigationen im Browser ausgelöst werden (z. B. horizontales Wischen zum Vor- und Zurückgehen oder Ziehen zum Aktualisieren).
2. Tastaturfokus und Barrierefreiheit beibehalten
Da auf einer 2D-Schwenkfläche oft keine fokussierbaren untergeordneten Steuerelemente vorhanden sind (z. B. eine Karte oder ein vergrößertes Bild), sollten Sie dafür sorgen, dass Tastaturnutzer den Container fokussieren und mit den Pfeiltasten schwenken können. Fügen Sie dazu tabindex="0", ein geeignetes role und ein barrierefreies Label sowie eine sichtbare :focus-visible-Anzeige hinzu.
Beispielcode
<!-- 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;
}
Best Practices und Einschränkungen
- Wenden Sie sie auf den Scroll-Container selbst an, nicht auf einen übergeordneten Wrapper:
scroll-axis-lockwird nicht übernommen. Wenn Sie sie also auf einen Wrapper anwenden, hat das keine Auswirkungen auf untergeordnete Scroll-Container. - Overscroll eingrenzen und versehentliche Navigation verhindern: Fügen Sie dem 2D-Scroll-Container
overscroll-behavior: noneoderoverscroll-behavior: containhinzu, damit das Schwenken bis zu einer Grenze nicht auf die übergeordnete Seite übertragen wird oder die Zurück-/Vorwärts-Wischgesten des Browsers und die Gesten zum Aktualisieren durch Ziehen auslöst. - Verschachtelte eindimensionale Bereiche behalten das Railing automatisch bei: Da
scroll-axis-locknicht übernommen wird, behält eine Seitenleiste oder ein Prüfbereich, der in.pan-viewportverschachtelt ist, das Standardverhaltenscroll-axis-lock: autobei. autobei standardmäßigen vertikalen Feeds, Artikeln und eindimensionalen Listen beibehalten: Die Achsensperre verhindert ein unbeabsichtigtes Abdriften in die Senkrechte bei unidirektionalen Inhalten.- Legen Sie
touch-action: nonenicht bedingungslos fest:touch-actionsteuert, ob der Browser Touch-Gesten überhaupt verarbeitet (und hat keine Auswirkungen auf Trackpad- oder Mausrad-Eingaben). Wenn Sie es festlegen, wird das native Touch-Scrollen des Compositors deaktiviert. Wenden Sie es nur innerhalb von@supports not (scroll-axis-lock: none)an, wenn Sie den JavaScript-Pointer-Fallback implementieren. - Mit
scrollLeft/scrollTopschwenken, nie mit CSS-transform: Wenn Sie die Oberfläche mittransformverschieben, funktioniert das Element nicht mehr als Scrollcontainer. Dadurch werden native Scrollleisten, die Begrenzung, das Scrollen mit den Pfeiltasten,scroll-Ereignisse,IntersectionObserverund scrollgesteuerte Animationen beeinträchtigt.
Fallback-Strategien
Wählen Sie je nach den Anforderungen Ihrer Anwendung zwischen Progressive Verbesserung und einem JavaScript-Fallback:
Option 1: Progressive Verbesserung (ohne JavaScript)
Verwenden Sie die progressive Verbesserung, wenn das Beibehalten des nativen Touch-Scrollens, des Trägheitsmoments und des Pinch-to-Zoom-Effekts des Compositor-Threads wichtiger ist als das Eliminieren der Achsensperre in nicht unterstützten Browsern:
- Unterstützende Browser: Der Browser verhindert, dass die Heuristik für die Achsensperre („Railing“) während Scroll-Gesten auf dem 2D-Scroll-Container aktiviert wird.
- Nicht unterstützte Browser: Browser, die
scroll-axis-lock: nonenicht erkennen, ignorieren die Deklaration und behalten ihr standardmäßiges 2D-Scrollverhaltenautobei. Sie scrollen nativ über beide Achsen, während Gesten, die hauptsächlich entlang einer einzelnen Achse beginnen, weiterhin eingeschränkt werden.
Option 2: JavaScript-Fallback (wheel + pointermove mit scrollLeft/scrollTop)
Wenn uneingeschränktes zweidimensionales Schwenken in allen Browsern erforderlich ist, verwenden Sie @supports not (scroll-axis-lock: none) und CSS.supports('scroll-axis-lock', 'none'), um wheel- und Zeiger-Ziehgesten bedingt abzufangen.
Da wheel (e.deltaX, e.deltaY) und pointermove (e.clientX, e.clientY) rohe, nicht gesperrte 2D-Deltas verfügbar machen, bevor die Scroll-Engine des Browsers die Achsensperre anwendet, können Sie diese Deltas direkt auf scrollLeft und scrollTop anwenden:
- Warum
scrollLeft/scrollTopstatt CSStransform: Wenn SiescrollLeftundscrollTopaktualisieren, bleibt das Element ein echter Scrollcontainer. Der Browser beschränkt die Scrollpositionen automatisch auf[0, scrollWidth - clientWidth]und[0, scrollHeight - clientHeight], native Scrollleisten bleiben synchronisiert, das Scrollen mit den Pfeiltasten der Tastatur funktioniert weiterhin undscroll-Ereignisse undIntersectionObserverwerden normal ausgelöst. - Kompromisse: Wenn
wheelmit{ passive: false }abgefangen wird, wird das Scrollen in den Hauptthread verschoben. Wenntouch-action: noneauf Touchscreens festgelegt wird, wird das native Trägheitsmoment nachpointerupdeaktiviert (ebenso wie das native Zoomen durch Auf- und Zuziehen).
/* 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);
}