Swipe om te onthullen

Met patronen voor swipen om te onthullen kunnen gebruikers een rij of contentvenster op de voorgrond horizontaal verschuiven om verborgen UI te tonen, zoals contextuele actieknoppen (Vastzetten, Dempen, Archiveren, Verwijderen) of een secundair zijmenu, zonder automatisch een actie te activeren. In plaats van een actie uit te voeren tijdens het swipen, wordt de container geopend om interactieve bedieningselementen te tonen die de gebruiker kan inspecteren en activeren, of terugswipen om te sluiten.

Ga naar Swipen om te verwijderen voor een swipegebaar over de volledige breedte waarmee een item onmiddellijk wordt vastgelegd en gesloten nadat een drempelwaarde is overschreden. Ga naar Pull to Reveal voor meer informatie over hoe je een verborgen kop bovenaan een verticale scrollcontainer onthult.

Hoe wordt dit geïmplementeerd

Of je nu actieknoppen onthult op een lijstitem of een zijmenu naast een scrollbaar deelvenster, de mechanismen zijn hetzelfde: verpak de content op de voorgrond en het verborgen deelvenster in een buitenste horizontale scrollcontainer die is geconfigureerd met CSS Scroll Snap (scroll-snap-type: x mandatory) en overscroll-behavior: chain. De systeemeigen scroll-engine van de browser verwerkt momentum, vastzetten, scrollen met toetsenbordfocus en scrollketens zonder JavaScript-gebaarlisteners.

Stap 1: Markeer de swipe-wrapper, content en het onthulde deelvenster

Plaats de content op de voorgrond eerst en de onthulde bedieningselementen (actieknoppen of een zijmenu) daarna in de scrollcontainer .SwipeReveal. Omdat de content als eerste in de DOM-volgorde staat, begint de scrollcontainer natuurlijk bij scroll-offset 0, waarbij het deelvenster rechts buiten het scherm is verborgen.

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

Stap 2: Stel de horizontale snapcontainer en overscroll-behavior: chain in

Als je een element in een buitenste scrollcontainer plaatst, alleen om een swipe-to-reveal-effect te maken, wordt die buitenste wrapper een tussenliggende scroller in het document (of een andere bovenliggende scrollcontainer).

De 4 zoekwoorden van overscroll-behavior bepalen onafhankelijk van elkaar of overmatig scrollen wordt doorgegeven aan een bovenliggende scrollcontainer (scroll chaining) en of de container lokale overscroll-mogelijkheden toont, zoals elastisch uitrekken of een gloed aan de rand (lokaal grenseffect):

Waarde Scrollketen (wordt doorgegeven aan bovenliggend element) Lokaal grenseffect (elastiek/gloed) Rol in Swipe-to-Reveal
auto Ja Ja Standaardgedrag; houd dit aan voor scrollers voor interne content waar normale bouncefeedback gewenst is, maar vermijd dit voor de buitenste swipe-wrapper waar het menu stuitert
chain Ja Nee Gebruik dit op de buitenste .SwipeReveal-scroller, zodat overmatig scrollen wordt doorgegeven aan de bovenliggende scroller (zoals het document) zonder dat het menu of de actiestrip stuitert
contain Nee Ja Vangt scrollen binnen de container op terwijl er lokaal nog steeds wordt gestuiterd
none Nee Nee Blokkeert zowel lokaal stuiteren als scrollketens naar bovenliggende elementen

Maak van .SwipeReveal een raster met 2 kolommen (grid-template-columns: 100% max-content) waarbij .SwipeReveal-content wordt uitgelijnd op start en .SwipeReveal-panel wordt uitgelijnd op 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;
  }
}

Als je ook bedieningselementen aan de linkerkant (inline-start) wilt tonen met 3 kolommen (grid-template-columns: max-content 100% max-content) of 2 kolommen (max-content 100%), plaats je de leidende .SwipeReveal-panel vóór .SwipeReveal-content. Omdat .SwipeReveal-content scroll-initial-target: nearest heeft, rust de scrollcontainer in eerste instantie op de contentkolom in plaats van op het voorste deelvenster.

Stap 3: Zet focusbare contentvensters weer in beeld bij toetsenbordfocus

Omdat de knoppen of links in .SwipeReveal-panel in rust buiten het scherm vallen, wordt .SwipeReveal automatisch gescrold als je er met een toetsenbord naartoe gaat om de gefocuste bedieningselementen te tonen. Omdat .SwipeReveal-content 100% van de breedte van de buitenste container beslaat, blijft het grootste deel ervan binnen de scrollport, zelfs als het zijvenster is geopend. Als je op Shift+Tab drukt om de focus terug te zetten op .SwipeReveal-content (of op een focusbaar element daarin), wordt de track niet automatisch teruggescrold naar 0.

Luister naar focusin op .SwipeReveal-content en check event.target.matches(':focus-visible') zodat de toetsenbordfocus op het deelvenster of een onderliggend element terugspringt naar .SwipeReveal-content zonder aanwijzerklikken of slepen met aanraken te verstoren:

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

Best practices en valkuilen

  • WEL: Gebruik scroll-snap-type: x mandatory in plaats van proximity. Met proximity kan een lichte swipe de actieknoppen of het zijmenu gedeeltelijk zichtbaar laten in de ruststand.
  • WEL: Stel overscroll-behavior: chain in op de buitenste horizontale scroller voor swipen om te onthullen (.SwipeReveal) zodat scrollen kan worden gekoppeld aan de bovenliggende scroller zonder dat het onthulde menu of de actiestrip stuitert.
  • Laat overscroll-behavior: auto (de standaardwaarde) WEL staan in deelvensters met scrollbare content (.SwipeReveal-content.is-scrollable) waar je normale bouncefeedback wilt, terwijl je horizontale swipes nog steeds koppelt aan de buitenste swipe-scroller.
  • WEL: Implementeer onthulde acties en items in het zijmenu als echte focusbare DOM-bedieningselementen (<button>, <a>). Als .SwipeReveal-content focusbaar is of focusbare bedieningselementen bevat, koppel je een focusin-listener aan event.target.matches(':focus-visible') om scrollIntoView({ block: 'nearest', inline: 'start' }) aan te roepen, zodat het zijvenster wordt gesloten als je teruggaat naar het contentvenster zonder dat dit invloed heeft op de aanwijzerinteracties.
  • Onderschep GEEN wheel-, touchmove- of pointermove-gebeurtenissen in JavaScript om de content handmatig te vertalen of scroll chaining te emuleren. Onderschepping van gebaren in de primaire thread blokkeert scrollen op basis van de compositor, verbreekt het systeemeigen scrollmomentum en vermindert de reactiesnelheid.

Reserve

Als een .SwipeReveal-container een .SwipeReveal-panel aan de linkerkant (inline-start) vóór .SwipeReveal-content plaatst, geven browsers die scroll-initial-target: nearest niet ondersteunen, de container in eerste instantie weer met de scroll-offset 0, waarbij het voorste deelvenster wordt getoond.

Als je Baseline-doel scroll-initial-target niet ondersteunt, detecteer je ondersteuning met CSS.supports('scroll-initial-target', 'nearest') en roep je scrollTo() aan met behavior: 'instant' op load, zodat containers met een voorloopvenster rechtstreeks naar .SwipeReveal-content springen zonder de pagina te scrollen:

Progressive enhancement

Behandel overscroll-behavior: chain en scrollbar-width: none als progressive enhancements zonder JavaScript-fallbacks:

  • Automatische CSS-cascade-fallback: De beginwaarde van overscroll-behavior is auto. Browsers die het zoekwoord chain nog niet herkennen, negeren overscroll-behavior: chain tijdens het parseren en behouden auto. Omdat auto al scroll chaining naar bovenliggende scrollcontainers toestaat, werken zowel het veeggebaar om te onthullen als scroll chaining naar de bovenliggende container standaard in alle browsers. Ondersteunende browsers onderdrukken gewoon de ongewenste rubberband-bounce op de buitenste veegwrapper.
  • contain of none overschrijven: Als een basisstylesheet of hulpprogrammaklasse overscroll-behavior: contain of none instelt, declareer je overscroll-behavior: auto onmiddellijk vóór overscroll-behavior: chain, zodat browsers die chain nog niet ondersteunen, nog steeds scrollen naar de bovenliggende container in plaats van deze vast te zetten.
  • Scrollbalk verbergen (scrollbar-width: none): Het verbergen van de horizontale scrollbalk is puur cosmetisch. Als een oudere engine scrollbar-width: none negeert, blijft de container voor swipen om te onthullen volledig functioneel met een standaard scrollbalk.