Gdy użytkownik przewija dwuwymiarowy kontener przewijania, przeglądarki zwykle stosują heurystykę blokowania osi przewijania (nazywaną też „railing”). Jeśli gest zaczyna się od znacznie większego ruchu wzdłuż jednej osi niż osi prostopadłej, przeglądarka blokuje przewijanie do tej pojedynczej osi poziomej lub pionowej i ignoruje prostopadłe zmiany – często przez pozostałą część gestu.
Blokowanie osi zapobiega przypadkowemu przesuwaniu się na boki w jednowymiarowych przepływach odczytu, ale powoduje problemy na dwuwymiarowych powierzchniach przesuwania, takich jak mapy interaktywne, powiększone obrazy, nieskończone obszary robocze, wykresy węzłów i duże siatki danych. Blokada utrzymuje się przez cały czas trwania gestu, więc wpływa na każde przesuwanie, które obejmuje 2 kierunki – niezależnie od tego, czy przesuwanie odbywa się jednocześnie w obu osiach (przesuwanie po przekątnej), czy po kolei w ramach jednego gestu (np. po ścieżkach zakrzywionych, okrężnych lub w kształcie litery L). Właściwość CSS scroll-axis-lock wyłącza tę heurystykę bezpośrednio w natywnym kontenerze przewijania, umożliwiając nieograniczone przesuwanie w 2 wymiarach przy zachowaniu przewijania w wątku kompozytora, fizyki pędu, natywnych pasków przewijania i nawigacji za pomocą klawiatury.
Implementacja
1. Wyłącz blokowanie osi i ogranicz przewijanie poza obszar w dwuwymiarowym kontenerze przewijania
Zastosuj scroll-axis-lock: none bezpośrednio do elementu, który tworzy dwuwymiarowy kontener przewijania (overflow: auto lub overflow: scroll). Chociaż dwuwymiarowy kontener przewijania może już aktualizować zmiany przewijania w obu osiach przy domyślnej wartości auto, auto umożliwia uruchomienie heurystyki blokowania przeglądarki, gdy gest zaczyna się głównie w jednej osi. Ustawienie scroll-axis-lock: none zapobiega stosowaniu tej funkcji na ekranach dotykowych, trackpadach i kółkach myszy.
Połącz scroll-axis-lock: none z overscroll-behavior: none (lub overscroll-behavior: contain) w kontenerze przewijania, aby po dotarciu do krawędzi nie następowało przewijanie strony nadrzędnej ani przypadkowe przewijanie w przeglądarce (np. poziome przesunięcie palcem do tyłu/przodu lub przeciągnięcie w dół, aby odświeżyć).
2. Zachowanie możliwości zaznaczania i ułatwień dostępu związanych z klawiaturą
Ponieważ dwuwymiarowa powierzchnia do przesuwania często nie ma elementów podrzędnych, które można zaznaczyć (np. mapy lub powiększonego obrazu), upewnij się, że użytkownicy klawiatury mogą zaznaczyć kontener i przesuwać go za pomocą klawiszy strzałek. W tym celu dodaj tabindex="0", odpowiedni role i dostępną etykietę oraz widoczny wskaźnik :focus-visible.
Przykładowy kod
<!-- 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;
}
Sprawdzone metody i ograniczenia
- Zastosuj ją do samego kontenera przewijania, a nie do nadrzędnego kontenera: właściwość
scroll-axis-locknie jest dziedziczona, więc ustawienie jej w kontenerze nie ma wpływu na kontenery przewijania podrzędne. - Ograniczanie przewijania poza granice i zapobieganie przypadkowej nawigacji: dodaj
overscroll-behavior: noneluboverscroll-behavior: containdo kontenera przewijania 2D, aby przesuwanie do granicy nie powodowało przejścia do strony nadrzędnej ani nie aktywowało nawigacji gestem przesunięcia do tyłu/do przodu w przeglądarce i gestów przeciągnięcia w celu odświeżenia. - Zagnieżdżone panele jednowymiarowe automatycznie zachowują poręcze: ponieważ
scroll-axis-locknie jest dziedziczony, pasek boczny lub panel inspektora zagnieżdżony w.pan-viewportzachowuje domyślne działaniescroll-axis-lock: auto. - Utrzymuj
autow standardowych kanałach pionowych, artykułach i listach jednowymiarowych: blokowanie osi zapobiega niezamierzonemu przesunięciu prostopadłemu w treściach jednokierunkowych. - Nie ustawiaj
touch-action: nonebezwarunkowo:touch-actionokreśla, czy przeglądarka w ogóle obsługuje gesty dotykowe (i nie ma wpływu na dane wejściowe z trackpada ani kółka), więc ustawienie tej opcji wyłącza natywne przewijanie dotykowe kompozytora. Stosuj go tylko wewnątrz elementu@supports not (scroll-axis-lock: none)podczas implementowania zastępczego wskaźnika JavaScript. - Przesuwaj za pomocą
scrollLeft/scrollTop, nigdy za pomocą CSStransform: przesuwanie powierzchni za pomocątransformuniemożliwia działanie elementu jako kontenera przewijania, co powoduje uszkodzenie natywnych pasków przewijania, blokowania granic, przewijania za pomocą klawiszy strzałek na klawiaturze, zdarzeńscroll,IntersectionObserveri animacji sterowanych przewijaniem.
Strategie zastępcze
Wybierz progresywne ulepszanie lub zastępczy kod JavaScript w zależności od wymagań aplikacji:
Opcja 1. Progresywne ulepszanie (bez JavaScriptu)
Używaj progresywnego ulepszania, gdy zachowanie natywnego przewijania dotykowego w wątku kompozytora, pędu bezwładnościowego i powiększania przez rozciąganie jest ważniejsze niż wyeliminowanie blokowania osi w nieobsługiwanych przeglądarkach:
- Obsługiwane przeglądarki: przeglądarka zapobiega uruchamianiu heurystyki blokowania osi („railing”) w kontenerze przewijania 2D podczas gestów przewijania.
- Nieobsługiwane przeglądarki: przeglądarki, które nie rozpoznają
scroll-axis-lock: none, bezpiecznie ignorują deklarację i zachowują domyślne zachowanie przewijania 2D –auto– przewijanie w obu osiach natywnie, przy jednoczesnym ograniczaniu gestów, które zaczynają się głównie wzdłuż jednej osi.
Opcja 2. Kreacja zastępcza JavaScript (wheel + pointermove z scrollLeft/scrollTop)
Jeśli wymagane jest nieograniczone dwuwymiarowe przesuwanie we wszystkich przeglądarkach, użyj @supports not (scroll-axis-lock: none) i CSS.supports('scroll-axis-lock', 'none'), aby warunkowo przechwytywać wheel i gesty przeciągania wskaźnika.
Ponieważ wheel (e.deltaX, e.deltaY) i pointermove (e.clientX, e.clientY) ujawniają nieprzetworzone, odblokowane delty 2D, zanim mechanizm przewijania przeglądarki zastosuje blokadę osi, możesz zastosować te delty bezpośrednio do scrollLeft i scrollTop:
- Dlaczego
scrollLeft/scrollTopzamiast CSStransform: aktualizacjascrollLeftiscrollTopsprawia, że element pozostaje prawdziwym kontenerem przewijania. Przeglądarka automatycznie ogranicza pozycje przewijania do[0, scrollWidth - clientWidth]i[0, scrollHeight - clientHeight], natywne paski przewijania pozostają zsynchronizowane, przewijanie za pomocą klawiszy strzałek na klawiaturze nadal działa, a zdarzeniascrolliIntersectionObserversą wywoływane normalnie. - Kompromisy: przechwytywanie zdarzenia
wheelza pomocą{ passive: false }przenosi przewijanie do głównego wątku, a ustawienietouch-action: nonena ekranach dotykowych wyłącza natywny pęd bezwładnościowy po zdarzeniupointerup(oraz natywne powiększanie przez rozciąganie).
/* 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);
}