Cuando un usuario se desplaza por un contenedor de desplazamiento bidimensional, los navegadores suelen aplicar una heurística de bloqueo del eje de desplazamiento (también conocida como "railing"). Si un gesto comienza con un movimiento significativamente mayor a lo largo de un eje que el eje perpendicular, el navegador bloquea el desplazamiento a ese único eje horizontal o vertical y, a menudo, ignora los deltas perpendiculares durante el resto del gesto.
Si bien el bloqueo de ejes evita el desplazamiento lateral accidental en los flujos de lectura unidimensionales, crea fricción en las superficies de desplazamiento bidimensionales, como mapas interactivos, imágenes ampliadas, lienzos infinitos, gráficos de nodos y cuadrículas de datos grandes. Como el bloqueo persiste durante todo el gesto, afecta cualquier desplazamiento que implique dos direcciones, ya sea que ambos ejes se desplacen simultáneamente (desplazamiento diagonal) o uno después del otro dentro de un solo gesto (como rutas curvas, circulares o en forma de L). La propiedad scroll-axis-lock de CSS inhabilita esta heurística directamente en un contenedor de desplazamiento nativo, lo que permite un desplazamiento bidimensional sin restricciones y, al mismo tiempo, conserva el desplazamiento del subproceso del compositor, la física de impulso, las barras de desplazamiento nativas y la navegación con el teclado.
Implementación
1. Inhabilitar el bloqueo de ejes y contener el sobredesplazamiento en el contenedor de desplazamiento bidimensional
Aplica scroll-axis-lock: none directamente al elemento que establece el contenedor de desplazamiento bidimensional (overflow: auto o overflow: scroll). Si bien un contenedor de desplazamiento 2D ya puede actualizar los deltas de desplazamiento en ambos ejes con el valor predeterminado auto, auto permite que se active la heurística de barandilla del navegador cuando un gesto comienza predominantemente en un eje. Si se establece en scroll-axis-lock: none, se evita que se active la barandilla en pantallas táctiles, paneles táctiles y ruedas del mouse.
Vincula scroll-axis-lock: none con overscroll-behavior: none (o overscroll-behavior: contain) en el contenedor de desplazamiento para que, cuando se llegue a un borde, no se encadene el desplazamiento a la página superior ni se activen navegaciones accidentales de sobredesplazamiento del navegador (como la navegación horizontal con deslizamiento hacia atrás o hacia adelante, o el gesto de deslizar hacia abajo para actualizar).
2. Preserva la capacidad de enfoque y la accesibilidad del teclado
Como una superficie de desplazamiento lateral en 2D suele carecer de controles secundarios enfocables (por ejemplo, un mapa o una imagen ampliada), asegúrate de que los usuarios de teclado puedan enfocar el contenedor y desplazarse lateralmente por él con las teclas de flecha. Para ello, agrega tabindex="0", un role adecuado, una etiqueta accesible y un indicador :focus-visible visible.
Código de ejemplo
<!-- 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;
}
Prácticas recomendadas y restricciones
- Aplícalo al contenedor de desplazamiento, no a un wrapper superior:
scroll-axis-lockno se hereda, por lo que establecerlo en un wrapper no tiene efecto en los contenedores de desplazamiento descendientes. - Contén el sobredesplazamiento y evita las navegaciones accidentales: Agrega
overscroll-behavior: noneooverscroll-behavior: containal contenedor de desplazamiento en 2D para que el desplazamiento lateral hacia un límite no se encadene a la página superior ni active la navegación con deslizamiento hacia atrás o adelante del navegador ni los gestos de deslizar hacia abajo para actualizar. - Los paneles unidimensionales anidados mantienen la barandilla automáticamente: Como
scroll-axis-lockno se hereda, una barra lateral o un panel de inspector anidado dentro de.pan-viewportconserva el comportamiento predeterminado descroll-axis-lock: auto. - Mantén
autoen feeds verticales, artículos y listas unidimensionales estándar: El bloqueo de ejes evita el desplazamiento perpendicular involuntario en el contenido unidireccional. - No configures
touch-action: nonede forma incondicional:touch-actioncontrola si el navegador controla los gestos táctiles (y no tiene efecto en la entrada del panel táctil o de la rueda), por lo que, si lo configuras, se inhabilita el desplazamiento táctil nativo del compositor. Solo aplícalo dentro de@supports not (scroll-axis-lock: none)cuando implementes el resguardo del puntero de JavaScript. - Desplázate con
scrollLeft/scrollTop, nunca con CSStransform: Traducir la superficie contransformimpide que el elemento funcione como un contenedor de desplazamiento, lo que interrumpe las barras de desplazamiento nativas, la fijación de límites, el desplazamiento con las teclas de flecha del teclado, los eventosscroll,IntersectionObservery las animaciones basadas en el desplazamiento.
Estrategias de resguardo
Elige entre la mejora progresiva y un resguardo de JavaScript según los requisitos de tu aplicación:
Opción 1: Mejora progresiva (sin JavaScript)
Usa la mejora progresiva cuando preservar el desplazamiento táctil nativo del subproceso del compositor, el impulso de desplazamiento inercial y el gesto de pellizcar para hacer zoom sea más importante que eliminar el bloqueo de ejes en los navegadores no compatibles:
- Navegadores compatibles: El navegador evita que se active su heurística de bloqueo de eje ("barandilla") en el contenedor de desplazamiento 2D durante los gestos de desplazamiento.
- Navegadores no compatibles: Los navegadores que no reconocen
scroll-axis-lock: noneignoran de forma segura la declaración y mantienen su comportamiento predeterminado de desplazamiento en 2D deauto, es decir, se desplazan por ambos ejes de forma nativa, mientras que siguen limitando los gestos que comienzan predominantemente a lo largo de un solo eje.
Opción 2: Resguardo de JavaScript (wheel + pointermove con scrollLeft/scrollTop)
Si se requiere un desplazamiento bidimensional sin restricciones en todos los navegadores, usa @supports not (scroll-axis-lock: none) y CSS.supports('scroll-axis-lock', 'none') para interceptar de forma condicional los gestos de arrastre del puntero y wheel.
Debido a que wheel (e.deltaX, e.deltaY) y pointermove (e.clientX, e.clientY) exponen diferencias 2D sin procesar y desbloqueadas antes de que el motor de desplazamiento del navegador aplique el bloqueo de ejes, puedes aplicar esas diferencias directamente a scrollLeft y scrollTop:
- Por qué
scrollLeft/scrollTopen lugar de CSStransform: ActualizarscrollLeftyscrollTopmantiene el elemento como un verdadero contenedor de desplazamiento. El navegador fija automáticamente las posiciones de desplazamiento en[0, scrollWidth - clientWidth]y[0, scrollHeight - clientHeight], las barras de desplazamiento nativas permanecen sincronizadas, el desplazamiento con las teclas de flecha del teclado sigue funcionando y los eventosscrollyIntersectionObserverse activan con normalidad. - Compensaciones: Interceptar
wheelcon{ passive: false }mueve el desplazamiento al subproceso principal, y configurartouch-action: noneen pantallas táctiles inhabilita el impulso de deslizamiento inercial nativo después depointerup(así como el gesto nativo de pellizcar para hacer zoom).
/* 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);
}