Als een gebruiker een tweedimensionale scrollcontainer scrollt, passen browsers meestal een heuristiek voor scrollasvergrendeling toe (ook wel 'railing' genoemd). Als een gebaar begint met aanzienlijk meer beweging langs één as dan de loodrechte as, vergrendelt de browser het scrollen naar die ene horizontale of verticale as en negeert de loodrechte delta's, vaak voor de rest van het gebaar.
Asvergrendeling voorkomt onbedoelde zijwaartse bewegingen in eendimensionale leesstromen, maar veroorzaakt wrijving op tweedimensionale pandoelen, zoals interactieve kaarten, ingezoomde afbeeldingen, oneindige canvassen, knooppuntdiagrammen en grote gegevensrasters. Omdat de vergrendeling tijdens het gebaar blijft bestaan, heeft deze invloed op alle pannen in 2 richtingen, of beide assen nu tegelijkertijd worden gepand (diagonaal pannen) of de ene na de andere binnen één gebaar (zoals gebogen, cirkelvormige of L-vormige paden). De CSS-eigenschap scroll-axis-lock zet deze heuristiek rechtstreeks uit in een systeemeigen scrollcontainer. Zo kun je onbeperkt tweedimensionaal pannen terwijl scrollen in de compositor-thread, momentumfysica, systeemeigen scrollbalken en toetsenbordnavigatie behouden blijven.
Implementatie
1. Zet asvergrendeling uit en beperk overscrollen in de tweedimensionale scrollcontainer
Pas scroll-axis-lock: none rechtstreeks toe op het element dat de tweedimensionale scrollcontainer (overflow: auto of overflow: scroll) vormt. Hoewel een 2D-scrollcontainer de scroll-delta's al langs beide assen kan updaten onder de standaardwaarde auto, zorgt auto ervoor dat de railing-heuristiek van de browser wordt geactiveerd als een gebaar voornamelijk langs één as begint. De instelling scroll-axis-lock: none voorkomt dat deze railing wordt geactiveerd op touchscreens, trackpads en muiswielen.
Koppel scroll-axis-lock: none aan overscroll-behavior: none (of overscroll-behavior: contain) in de scrollcontainer, zodat de gebruiker niet naar de bovenliggende pagina scrollt of per ongeluk overscrollnavigatie in de browser activeert (zoals horizontaal swipen om terug of vooruit te gaan of trekken om te vernieuwen) als de gebruiker een rand bereikt.
2. Toetsenbordfocus en -toegankelijkheid behouden
Omdat een 2D-panningoppervlak vaak geen focusbare onderliggende bedieningselementen heeft (bijvoorbeeld een kaart of een ingezoomde afbeelding), zorg je dat toetsenbordgebruikers de focus op de container kunnen plaatsen en kunnen pannen met de pijltoetsen door tabindex="0", een toepasselijk role en toegankelijk label en een zichtbare :focus-visible-indicator toe te voegen.
Voorbeeldcode
<!-- 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 en beperkingen
- Pas de eigenschap toe op de scrollcontainer zelf, niet op een bovenliggende wrapper:
scroll-axis-lockwordt niet overgenomen, dus als je deze instelt op een wrapper, heeft dit geen effect op onderliggende scrollcontainers. - Overscroll beperken en onbedoelde navigatie voorkomen: Voeg
overscroll-behavior: noneofoverscroll-behavior: containtoe aan de 2D-scrollcontainer, zodat pannen naar een grens niet wordt gekoppeld aan de bovenliggende pagina of de swipe-navigatie vooruit/terug en de gebaren voor trek omlaag om te vernieuwen van de browser activeert. - Geneste eendimensionale deelvensters behouden automatisch de railing: Omdat
scroll-axis-lockniet wordt overgenomen, behoudt een zijbalk of inspectiedeelvenster dat is genest in.pan-viewporthet standaardgedrag vanscroll-axis-lock: auto. - Houd
autoaan voor standaard verticale feeds, artikelen en eendimensionale lijsten: Asvergrendeling voorkomt onbedoelde loodrechte afwijking in content die in één richting beweegt. - Stel
touch-action: noneniet onvoorwaardelijk in:touch-actionbepaalt of de browser aanraakgebaren verwerkt (en heeft geen invloed op trackpad- of muisinvoer). Als je deze instelling instelt, wordt scrollen via aanraken van de systeemeigen compositor uitgezet. Pas dit alleen toe binnen@supports not (scroll-axis-lock: none)als u de JavaScript-pointer-fallback implementeert. - Pan met
scrollLeft/scrollTop, nooit CSStransform: Als je het oppervlak vertaalt mettransform, werkt het element niet meer als scrollcontainer. Hierdoor werken systeemeigen scrollbalken, grensklemming, scrollen met de pijltoetsen op het toetsenbord,scroll-gebeurtenissen,IntersectionObserveren scrollgestuurde animaties niet meer.
Reservestrategieën
Kies tussen progressive enhancement en een JavaScript-fallback op basis van de vereisten van je app:
Optie 1: Progressive enhancement (geen JavaScript)
Gebruik progressive enhancement als het behoud van native compositor-thread touch scrolling, traagheidsmomentum en knijpen om te zoomen belangrijker is dan het elimineren van asvergrendeling in niet-ondersteunde browsers:
- Ondersteunende browsers: De browser voorkomt dat de heuristiek voor asvergrendeling ('railing') wordt geactiveerd in de 2D-scrollcontainer tijdens scrollgebaren.
- Niet-ondersteunde browsers: Browsers die
scroll-axis-lock: noneniet herkennen, negeren de declaratie veilig en behouden hun standaardauto2D-scrollgedrag. Ze scrollen standaard over beide assen, terwijl ze nog steeds gebaren volgen die voornamelijk langs één as beginnen.
Optie 2: JavaScript-reserve (wheel + pointermove met scrollLeft/scrollTop)
Als onbeperkt tweedimensionaal pannen in alle browsers is vereist, gebruik je @supports not (scroll-axis-lock: none) en CSS.supports('scroll-axis-lock', 'none') om wheel en aanwijzer-sleepbewegingen voorwaardelijk te onderscheppen.
Omdat wheel (e.deltaX, e.deltaY) en pointermove (e.clientX, e.clientY) onbewerkte, ontgrendelde 2D-delta's tonen voordat de scroll-engine van de browser asvergrendeling toepast, kun je die delta's rechtstreeks toepassen op scrollLeft en scrollTop:
- Waarom
scrollLeft/scrollTopin plaats van CSStransform: Als jescrollLeftenscrollTopupdatet, blijft het element een echte scrollcontainer. De browser beperkt de scrollposities automatisch tot[0, scrollWidth - clientWidth]en[0, scrollHeight - clientHeight], systeemeigen scrollbalken blijven gesynchroniseerd, scrollen met de pijltoetsen op het toetsenbord blijft werken enscroll-gebeurtenissen enIntersectionObserverworden normaal geactiveerd. - Afwegingen: Als je
wheelonderschept met{ passive: false }, wordt scrollen verplaatst naar de hoofdthread. Als jetouch-action: noneinstelt op touchscreens, wordt het systeemeigen traagheidsmoment napointerupuitgezet (net als systeemeigen knijpen om te zoomen).
/* 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);
}