Desliza para mostrar

Los patrones de deslizar para revelar permiten que los usuarios deslicen una fila en primer plano o un panel de contenido de forma horizontal para mostrar la IU oculta, como los botones de acción contextuales (Fijar, Silenciar, Archivar, Borrar) o un menú lateral secundario, sin activar automáticamente ninguna acción. En lugar de confirmar una acción durante el deslizamiento, el contenedor se abre para revelar controles interactivos que el usuario puede inspeccionar y activar, o bien deslizar hacia atrás para cerrar.

Para ver un gesto de deslizamiento de ancho completo que confirma y descarta de inmediato un elemento que supera un umbral, consulta Desliza para quitar. Para revelar un encabezado oculto en la parte superior de un contenedor de desplazamiento vertical, consulta Deslizar para revelar.

Cómo implementarlo

Ya sea que muestres botones de acción en un elemento de lista o un menú lateral junto a un panel desplazable, la mecánica es idéntica: une el contenido en primer plano y el panel oculto dentro de un contenedor de desplazamiento horizontal externo configurado con CSS Scroll Snap (scroll-snap-type: x mandatory) y overscroll-behavior: chain. El motor de desplazamiento nativo del navegador controla el impulso, el ajuste, el desplazamiento del enfoque del teclado y el encadenamiento de desplazamiento sin objetos de escucha de gestos de JavaScript.

Paso 1: Marca el wrapper de deslizamiento, el contenido y el panel que se muestra

Coloca el contenido en primer plano primero y los controles revelados (botones de acción o un menú lateral) en segundo lugar dentro del contenedor de desplazamiento .SwipeReveal. Como el contenido es el primero en el orden del DOM, el contenedor de desplazamiento comienza de forma natural en el desplazamiento 0 con el panel oculto fuera de la pantalla a la derecha.

<!-- Example A: List item with revealable action buttons -->
<div class="SwipeReveal">
  <div class="SwipeReveal-content">
    <strong>Design review notes</strong>
    <p>Updated component specs for the Q3 release.</p>
  </div>
  <div class="SwipeReveal-panel" role="group" aria-label="Item actions">
    <button type="button">Pin</button>
    <button type="button">Archive</button>
    <button type="button">Delete</button>
  </div>
</div>

<!-- Example B: Scrollable content pane with a revealable side menu -->
<div class="SwipeReveal" role="region" aria-label="Document preview with swipeable side menu">
  <div class="SwipeReveal-content is-scrollable" tabindex="0">
    <h2>Article Preview</h2>
    <p>Scroll vertically through this pane, or swipe left to reveal the side menu...</p>
  </div>
  <nav class="SwipeReveal-panel" aria-label="Contextual menu">
    <a href="#share">Share</a>
    <a href="#bookmark">Bookmark</a>
    <a href="#export">Export PDF</a>
  </nav>
</div>

Paso 2: Configura el contenedor de ajuste horizontal y overscroll-behavior: chain

Cuando encierras un elemento en un contenedor de desplazamiento externo solo para crear un efecto de deslizar para revelar, ese contenedor externo se convierte en un desplazador intermedio dentro del documento (o en otro contenedor de desplazamiento superior).

Las cuatro palabras clave de overscroll-behavior controlan de forma independiente si el desplazamiento excesivo se propaga a un contenedor de desplazamiento superior (encadenamiento de desplazamiento) y si el contenedor muestra affordances de sobredesplazamiento locales, como el estiramiento elástico de la banda elástica o el resplandor de los bordes (efecto de límite local):

Valor Encadenamiento de desplazamiento (se propaga al elemento superior) Efecto de límite local (banda elástica/resplandor) Función en Deslizar para revelar
auto Sí Sí Comportamiento predeterminado; mantén esta opción en los desplazadores de contenido internos en los que se desea obtener comentarios normales de rebote, pero evítala en el wrapper de deslizamiento externo, donde hace que el menú rebote
chain Sí No Se usa en el desplazador .SwipeReveal exterior para que el exceso de desplazamiento se encadene en el desplazador superior (como el documento) sin que el menú o la tira de acciones reboten.
contain No Sí Atrapa el desplazamiento dentro del contenedor mientras sigue rebotando de forma local
none No No Bloquea el rebote local y el encadenamiento de desplazamiento a los elementos superiores

Haz que .SwipeReveal sea una cuadrícula de dos columnas (grid-template-columns: 100% max-content) en la que .SwipeReveal-content se ajuste a start y .SwipeReveal-panel se ajuste a end:

.SwipeReveal {
  /* Column 1: full-width content. Column 2: auto-sized actions or side menu. */
  display: grid;
  grid-template-columns: 100% max-content;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Prevent the swipe wrapper and revealed panel from bouncing locally while
     still chaining excess scroll into the parent scroller (the document). */
  overscroll-behavior: chain;
}

@media (prefers-reduced-motion: no-preference) {
  .SwipeReveal {
    scroll-behavior: smooth;
  }
}

.SwipeReveal-content {
  scroll-snap-align: start;
  scroll-initial-target: nearest;
  background: Canvas;
}

.SwipeReveal-content:focus-visible {
  outline: auto;
  outline-offset: -2px;
}

.SwipeReveal-content.is-scrollable {
  block-size: 20rem;
  overflow: auto;
  /* Keeps default `overscroll-behavior: auto` so the content pane retains
     its normal bounce effect while chaining horizontal swipes to `.SwipeReveal`. */
}

.SwipeReveal-panel {
  /* Snapping a trailing panel to `end` (or a leading panel to `start`) brings
     all controls fully into view while keeping the adjacent edge of the content visible. */
  scroll-snap-align: end;
  display: flex;

  &:first-child {
    scroll-snap-align: start;
  }
}

Si también quieres revelar los controles del lado izquierdo (inline-start) con tres columnas (grid-template-columns: max-content 100% max-content) o dos columnas (max-content 100%), coloca el .SwipeReveal-panel inicial antes de .SwipeReveal-content. Debido a que .SwipeReveal-content tiene scroll-initial-target: nearest, el contenedor de desplazamiento se ubica inicialmente en la columna de contenido, en lugar de en el panel principal.

Paso 3: Ajusta los paneles de contenido enfocables para que vuelvan a la vista cuando se enfoque el teclado

Debido a que los botones o vínculos dentro de .SwipeReveal-panel están fuera de la pantalla en reposo, cuando se presiona la tecla Tab para acceder a ellos con un teclado, se desplaza automáticamente .SwipeReveal para revelar el control enfocado. Sin embargo, como .SwipeReveal-content abarca 100% del ancho del contenedor exterior, la mayor parte permanece dentro del puerto de desplazamiento incluso cuando el panel lateral está abierto, por lo que presionar Shift+Tab para volver a enfocar .SwipeReveal-content (o un elemento enfocable dentro de él) no desplazará automáticamente la pista de vuelta a 0.

Escucha focusin en .SwipeReveal-content y verifica event.target.matches(':focus-visible') para que el enfoque del teclado en el panel o cualquier descendiente vuelva a colocar .SwipeReveal-content al principio sin interferir con los clics del puntero o los arrastres táctiles:

for (const content of document.querySelectorAll('.SwipeReveal-content')) {
  content.addEventListener('focusin', (event) => {
    if (event.target.matches(':focus-visible')) {
      content.scrollIntoView({
        block: 'nearest',
        inline: 'start',
      });
    }
  });
}

Prácticas recomendadas y errores comunes

  • SÍ uses scroll-snap-type: x mandatory en lugar de proximity. Con proximity, un deslizamiento ligero puede dejar los botones de acción o el menú lateral parcialmente expuestos en reposo.
  • SÍ establece overscroll-behavior: chain en el desplazador horizontal externo de deslizamiento para revelar (.SwipeReveal) de modo que el desplazamiento pueda encadenarse al desplazador superior sin que el menú revelado o la tira de acciones reboten.
  • SÍ dejes overscroll-behavior: auto (el valor predeterminado) en los paneles de contenido desplazable internos (.SwipeReveal-content.is-scrollable) en los que quieras que se produzcan comentarios de rebote normales y, al mismo tiempo, encadenar los deslizamientos horizontales al desplazador de deslizamiento externo.
  • SÍ implementa acciones reveladas y elementos del menú lateral como controles del DOM reales que se puedan enfocar (<button>, <a>). Cuando .SwipeReveal-content se puede enfocar o contiene controles que se pueden enfocar, vincula un objeto de escucha focusin con event.target.matches(':focus-visible') para llamar a scrollIntoView({ block: 'nearest', inline: 'start' }), de modo que, cuando se vuelva a presionar la tecla Tab para volver al panel de contenido, se cierre el panel lateral sin afectar las interacciones del puntero.
  • NO interceptes eventos wheel, touchmove ni pointermove en JavaScript para traducir manualmente el contenido o emular el encadenamiento de desplazamiento. La interrupción de gestos en el subproceso principal bloquea el desplazamiento controlado por el compositor, interrumpe el impulso de desplazamiento nativo y disminuye la capacidad de respuesta.

Resguardo

Cuando un contenedor .SwipeReveal coloca un .SwipeReveal-panel en el lado izquierdo (inline-start) antes de .SwipeReveal-content, los navegadores que no admiten scroll-initial-target: nearest se renderizarán inicialmente en el desplazamiento 0 con el panel principal expuesto.

Si tu destino de Baseline no admite scroll-initial-target, detecta la compatibilidad con CSS.supports('scroll-initial-target', 'nearest') y llama a scrollTo() con behavior: 'instant' en load para que los contenedores con un panel principal salten directamente a .SwipeReveal-content sin desplazarse por la página:

Mejora progresiva

Trata overscroll-behavior: chain y scrollbar-width: none como mejoras progresivas sin resguardos de JavaScript:

  • Resguardo automático de cascada de CSS: El valor inicial de overscroll-behavior es auto. Los navegadores que aún no reconocen la palabra clave chain ignoran overscroll-behavior: chain en el momento del análisis y retienen auto. Debido a que auto ya permite el encadenamiento de desplazamiento a los contenedores de desplazamiento superiores, tanto el gesto de deslizar para revelar como el encadenamiento de desplazamiento al elemento superior funcionan en todos los navegadores de forma predeterminada. Los navegadores compatibles simplemente suprimen el rebote no deseado en el contenedor de deslizamiento exterior.
  • Anulación de contain o none: Si una hoja de estilo base o una clase de utilidad establece overscroll-behavior: contain o none, declara overscroll-behavior: auto inmediatamente antes de overscroll-behavior: chain para que los navegadores que aún no admiten chain sigan encadenando el desplazamiento al elemento superior en lugar de atraparlo.
  • Ocultamiento de la barra de desplazamiento (scrollbar-width: none): Ocultar la pista de la barra de desplazamiento horizontal es puramente estético. Si un motor más antiguo ignora scrollbar-width: none, el contenedor de deslizamiento para revelar sigue siendo completamente funcional con una barra de desplazamiento estándar.