Quando un utente scorre un contenitore di scorrimento bidimensionale, i browser in genere applicano un'euristica di blocco dell'asse di scorrimento (nota anche come "railing"). Se un gesto inizia con un movimento significativamente maggiore lungo un asse rispetto all'asse perpendicolare, il browser blocca lo scorrimento su quel singolo asse orizzontale o verticale e ignora i delta perpendicolari, spesso per il resto del gesto.
Sebbene il blocco dell'asse impedisca lo spostamento laterale accidentale nei flussi di lettura unidimensionali, crea attrito sulle superfici di panning bidimensionali come mappe interattive, immagini ingrandite, canvas infiniti, grafici dei nodi e griglie di dati di grandi dimensioni. Poiché il blocco persiste per tutto il gesto, influisce su qualsiasi panoramica che coinvolge due direzioni, indipendentemente dal fatto che la panoramica venga eseguita contemporaneamente su entrambi gli assi (panoramica diagonale) o uno dopo l'altro all'interno di un singolo gesto (ad esempio percorsi curvi, circolari o a forma di L). La proprietà CSS scroll-axis-lock disattiva questa euristica direttamente su un container di scorrimento nativo, consentendo la panoramica bidimensionale senza limitazioni e preservando lo scorrimento del thread del compositore, la fisica del momentum, le barre di scorrimento native e la navigazione da tastiera.
Implementazione
1. Disattiva il blocco degli assi e limita l'overscroll nel contenitore di scorrimento bidimensionale
Applica scroll-axis-lock: none direttamente all'elemento che stabilisce il container di scorrimento bidimensionale (overflow: auto o overflow: scroll). Sebbene un container di scorrimento 2D possa già aggiornare i delta di scorrimento lungo entrambi gli assi con il valore predefinito auto, auto consente l'attivazione dell'euristica di railing del browser quando un gesto inizia prevalentemente lungo un asse. L'impostazione scroll-axis-lock: none impedisce l'attivazione di questa barriera su touchscreen, trackpad e rotelline del mouse.
Associa scroll-axis-lock: none a overscroll-behavior: none (o overscroll-behavior: contain) sul contenitore di scorrimento in modo che il raggiungimento di un bordo non concateni lo scorrimento alla pagina principale o attivi navigazioni di overscroll accidentali del browser (come la navigazione con scorrimento orizzontale avanti/indietro o il pull to refresh).
2. Preserva la selezione della tastiera e l'accessibilità
Poiché una superficie di panning 2D spesso non dispone di controlli secondari attivabili (ad esempio una mappa o un'immagine ingrandita), assicurati che gli utenti della tastiera possano attivare il container ed eseguire il panning con i tasti Freccia aggiungendo tabindex="0", un role appropriato, un'etichetta accessibile e un indicatore :focus-visible visibile.
Codice di esempio
<!-- 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 practice e vincoli
- Applicala direttamente al container di scorrimento, non a un wrapper principale:
scroll-axis-locknon viene ereditata, quindi impostarla su un wrapper non ha alcun effetto sui container di scorrimento secondari. - Contenere l'overscroll e impedire navigazioni accidentali: aggiungi
overscroll-behavior: noneooverscroll-behavior: containal contenitore di scorrimento 2D in modo che la panoramica in un confine non si colleghi alla pagina principale o attivi la navigazione con scorrimento avanti/indietro del browser e i gesti di pull to refresh. - I pannelli monodimensionali nidificati mantengono automaticamente la ringhiera: poiché
scroll-axis-locknon eredita, un pannello laterale o di ispezione nidificato all'interno di.pan-viewportmantiene il comportamento predefinito discroll-axis-lock: auto. - Mantieni
autosu feed verticali standard, articoli ed elenchi unidimensionali: il blocco dell'asse impedisce lo spostamento perpendicolare involontario nei contenuti unidirezionali. - Non impostare
touch-action: nonein modo incondizionato:touch-actioncontrolla se il browser gestisce i gesti tattili (e non ha alcun effetto sull'input del trackpad o della rotellina), quindi l'impostazione disattiva lo scorrimento touch del compositore nativo. Applicalo solo all'interno di@supports not (scroll-axis-lock: none)quando implementi il fallback del puntatore JavaScript. - Panoramica con
scrollLeft/scrollTop, mai contransformCSS: la traslazione della superficie contransformimpedisce all'elemento di funzionare come contenitore di scorrimento, interrompendo le barre di scorrimento native, il blocco dei limiti, lo scorrimento con i tasti Freccia della tastiera, gli eventiscroll,IntersectionObservere le animazioni basate sullo scorrimento.
Strategie di riserva
Scegli tra il potenziamento progressivo e un fallback JavaScript in base ai requisiti della tua applicazione:
Opzione 1: potenziamento progressivo (senza JavaScript)
Utilizza il potenziamento progressivo quando preservare lo scorrimento touch nativo del thread del compositore, lo slancio inerziale e il gesto di pizzicare per aumentare lo zoom è più importante che eliminare il blocco dell'asse nei browser non supportati:
- Browser supportati: il browser impedisce l'attivazione della sua euristica di blocco degli assi ("railing") sul container di scorrimento 2D durante i gesti di scorrimento.
- Browser non supportati: i browser che non riconoscono
scroll-axis-lock: noneignorano in modo sicuro la dichiarazione e mantengono il comportamento di scorrimento 2D predefinito diauto, ovvero lo scorrimento su entrambi gli assi in modo nativo, pur mantenendo i gesti di scorrimento che iniziano prevalentemente lungo un singolo asse.
Opzione 2: fallback JavaScript (wheel + pointermove con scrollLeft/scrollTop)
Se è necessario un panning bidimensionale senza limitazioni in tutti i browser, utilizza @supports not (scroll-axis-lock: none) e CSS.supports('scroll-axis-lock', 'none') per intercettare in modo condizionale i gesti di trascinamento del puntatore e wheel.
Poiché wheel (e.deltaX, e.deltaY) e pointermove (e.clientX, e.clientY) espongono delta 2D non elaborati e sbloccati prima che il motore di scorrimento del browser applichi il blocco dell'asse, puoi applicare questi delta direttamente a scrollLeft e scrollTop:
- Perché
scrollLeft/scrollTopanziché CSStransform: l'aggiornamento discrollLeftescrollTopmantiene l'elemento come un vero e proprio contenitore di scorrimento. Il browser blocca automaticamente le posizioni di scorrimento su[0, scrollWidth - clientWidth]e[0, scrollHeight - clientHeight], le barre di scorrimento native rimangono sincronizzate, lo scorrimento con i tasti Freccia della tastiera continua a funzionare e gli eventiscrolleIntersectionObservervengono attivati normalmente. - Compromessi: l'intercettazione di
wheelcon{ passive: false }sposta lo scorrimento sul thread principale e l'impostazione ditouch-action: nonesui touchscreen disattiva l'inerzia nativa del fling dopopointerup(oltre al pinch-to-zoom nativo).
/* 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);
}