Balayer pour afficher

Les modèles de balayage pour afficher permettent aux utilisateurs de faire glisser horizontalement une ligne ou un volet de contenu au premier plan pour afficher l'interface utilisateur masquée, comme des boutons d'action contextuels (Épingler, Couper le son, Archiver, Supprimer) ou un menu latéral secondaire, sans activer automatiquement d'action. Au lieu d'effectuer une action pendant le balayage, le conteneur s'ouvre pour révéler des commandes interactives que l'utilisateur peut inspecter et activer, ou balayer vers l'arrière pour fermer.

Pour un geste de balayage sur toute la largeur qui valide et supprime immédiatement un élément au-delà d'un seuil, consultez Balayer pour supprimer. Pour afficher un en-tête masqué en haut d'un conteneur à défilement vertical, consultez Tirer pour afficher.

Mise en œuvre

Que vous affichiez des boutons d'action sur un élément de liste ou un menu latéral à côté d'un volet déroulant, le mécanisme est identique : encapsulez le contenu au premier plan et le panneau masqué dans un conteneur de défilement horizontal externe configuré avec CSS Scroll Snap (scroll-snap-type: x mandatory) et overscroll-behavior: chain. Le moteur de défilement natif du navigateur gère l'inertie, l'alignement, le défilement du focus clavier et le chaînage du défilement sans écouteurs de gestes JavaScript.

Étape 1 : Balisez le wrapper de balayage, le contenu et le panneau affiché

Placez d'abord le contenu au premier plan, puis les commandes révélées (boutons d'action ou menu latéral) dans le conteneur de défilement .SwipeReveal. Comme le contenu est le premier dans l'ordre du DOM, le conteneur de défilement commence naturellement au décalage de défilement 0, le panneau étant masqué hors de l'écran à droite.

<!-- 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>

Étape 2 : Configurer le conteneur d'alignement horizontal et overscroll-behavior: chain

Lorsque vous encapsulez un élément dans un conteneur de défilement externe uniquement pour créer un effet de balayage pour afficher, ce wrapper externe devient un défilement intermédiaire dans le document (ou un autre conteneur de défilement parent).

Les quatre mots clés de overscroll-behavior contrôlent indépendamment si le défilement hors limites se propage à un conteneur de défilement parent (chaînage de défilement) et si le conteneur affiche des affordances de défilement hors limites locales telles que l'étirement élastique ou le halo de bordure (effet de limite locale) :

Valeur Chaînage de défilement (se propage à l'ancêtre) Effet de limite locale (élastique/lueur) Rôle dans le balayage pour révéler
auto Oui Oui Comportement par défaut. Conservez cette option sur les défilements de contenu interne où un retour de rebond normal est souhaité, mais évitez-la sur le wrapper de balayage externe, car elle fait rebondir le menu.
chain Oui Non Utilisez-le sur le défilement .SwipeReveal externe afin que le défilement excessif soit chaîné dans le défilement parent (comme le document) sans que le menu ou la bande d'action ne rebondissent.
contain Non Oui Piège le défilement à l'intérieur du conteneur tout en conservant le rebond localement
none Non Non Bloque le rebond local et le chaînage du défilement vers les ancêtres

Transformez .SwipeReveal en grille à deux colonnes (grid-template-columns: 100% max-content) où .SwipeReveal-content s'aligne sur start et .SwipeReveal-panel s'aligne sur 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 vous souhaitez également afficher les commandes sur le côté gauche (inline-start) à l'aide de trois colonnes (grid-template-columns: max-content 100% max-content) ou de deux colonnes (max-content 100%), placez le .SwipeReveal-panel de début avant .SwipeReveal-content. Comme .SwipeReveal-content a la valeur scroll-initial-target: nearest, le conteneur de défilement repose initialement sur la colonne de contenu plutôt que sur le panneau de gauche.

Étape 3 : Réalignez les volets de contenu sélectionnables dans la vue lorsque le focus clavier est activé

Étant donné que les boutons ou les liens à l'intérieur de .SwipeReveal-panel sont hors écran au repos, l'utilisation de la touche de tabulation pour y accéder avec un clavier fait automatiquement défiler .SwipeReveal pour afficher la commande sélectionnée. Cependant, comme .SwipeReveal-content s'étend sur 100% de la largeur du conteneur externe, la majeure partie reste dans la fenêtre de défilement même lorsque le panneau latéral est ouvert. Par conséquent, appuyer sur Shift+Tab pour déplacer le curseur sur .SwipeReveal-content (ou sur un élément sélectionnable à l'intérieur) ne fera pas défiler automatiquement la piste vers 0.

Écoutez focusin sur .SwipeReveal-content et vérifiez event.target.matches(':focus-visible') pour que le focus clavier sur le volet ou tout descendant ramène .SwipeReveal-content au début sans interférer avec les clics du pointeur ou les glissements tactiles :

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

Bonnes pratiques et pièges à éviter

  • À FAIRE : Utilisez scroll-snap-type: x mandatory plutôt que proximity. Avec proximity, un balayage léger peut laisser les boutons d'action ou le menu latéral partiellement visibles au repos.
  • À FAIRE : Définissez overscroll-behavior: chain sur le défilement horizontal externe par balayage pour afficher (.SwipeReveal) afin que le défilement puisse s'enchaîner dans le défilement parent sans que le menu ou la bande d'action affichés ne rebondissent.
  • À FAIRE : laissez overscroll-behavior: auto (valeur par défaut) sur les volets de contenu à défilement internes (.SwipeReveal-content.is-scrollable) où vous souhaitez que le retour de rebond normal se produise tout en enchaînant les balayages horizontaux vers le défilement par balayage externe.
  • À FAIRE : Implémentez les actions révélées et les éléments du menu latéral en tant que véritables commandes DOM sélectionnables (<button>, <a>). Lorsque .SwipeReveal-content est sélectionnable ou contient des commandes sélectionnables, associez un écouteur focusin à event.target.matches(':focus-visible') pour appeler scrollIntoView({ block: 'nearest', inline: 'start' }) afin que le retour au volet de contenu ferme le panneau latéral sans affecter les interactions du pointeur.
  • NE PAS intercepter les événements wheel, touchmove ou pointermove en JavaScript pour traduire manuellement le contenu ou émuler le chaînage de défilement. L'interception des gestes du thread principal bloque le défilement piloté par le compositeur, interrompt l'élan de défilement natif et dégrade la réactivité.

Action de remplacement

Lorsqu'un conteneur .SwipeReveal place un .SwipeReveal-panel sur le côté gauche (inline-start) avant .SwipeReveal-content, les navigateurs qui ne sont pas compatibles avec scroll-initial-target: nearest s'affichent initialement au décalage de défilement 0 avec le panneau de gauche exposé.

Si votre cible Baseline n'est pas compatible avec scroll-initial-target, détectez la compatibilité avec CSS.supports('scroll-initial-target', 'nearest') et appelez scrollTo() avec behavior: 'instant' sur load pour que les conteneurs avec un panneau principal passent directement à .SwipeReveal-content sans faire défiler la page :

Amélioration progressive

Traitez overscroll-behavior: chain et scrollbar-width: none comme des améliorations progressives sans solutions de remplacement JavaScript :

  • Création de remplacement automatique en cascade CSS : la valeur initiale de overscroll-behavior est auto. Les navigateurs qui ne reconnaissent pas encore le mot clé chain ignorent overscroll-behavior: chain au moment de l'analyse et conservent auto. Comme auto autorise déjà le chaînage du défilement vers les conteneurs de défilement ancêtres, le geste de balayage pour afficher et le chaînage du défilement vers le parent fonctionnent par défaut dans tous les navigateurs. Les navigateurs compatibles suppriment simplement l'effet de rebond indésirable sur le conteneur de balayage externe.
  • Remplacement de contain ou none : si une feuille de style de base ou une classe d'utilitaire définit overscroll-behavior: contain ou none, déclarez overscroll-behavior: auto immédiatement avant overscroll-behavior: chain afin que les navigateurs qui ne sont pas encore compatibles avec chain continuent de chaîner le défilement au parent plutôt que de le piéger.
  • Masquage de la barre de défilement (scrollbar-width: none) : le masquage de la barre de défilement horizontale est purement esthétique. Si un moteur plus ancien ignore scrollbar-width: none, le conteneur de balayage pour afficher reste entièrement fonctionnel avec une barre de défilement standard.