Geser untuk membuka

Pola geser untuk menampilkan memungkinkan pengguna menggeser baris latar depan atau panel konten secara horizontal untuk menampilkan UI tersembunyi—seperti tombol tindakan kontekstual (Sematkan, Bisukan, Arsipkan, Hapus) atau menu samping sekunder—tanpa mengaktifkan tindakan apa pun secara otomatis. Alih-alih melakukan tindakan selama geser, penampung akan terbuka untuk menampilkan kontrol interaktif yang dapat diperiksa dan diaktifkan pengguna, atau geser kembali untuk menutup.

Untuk gestur geser lebar penuh yang langsung menerapkan dan menutup item setelah melewati batas, lihat Geser untuk menghapus. Untuk menampilkan header tersembunyi di bagian atas penampung scroll vertikal, lihat Tarik untuk Menampilkan.

Cara menerapkan

Baik Anda menampilkan tombol tindakan pada item daftar atau menu samping di samping panel yang dapat di-scroll, mekanismenya sama: gabungkan konten latar depan dan panel tersembunyi di dalam penampung scroll horizontal luar yang dikonfigurasi dengan CSS Scroll Snap (scroll-snap-type: x mandatory) dan overscroll-behavior: chain. Mesin scroll native browser menangani momentum, snapping, scroll fokus keyboard, dan perantaian scroll tanpa pemroses gestur JavaScript.

Langkah 1: Beri markup pada wrapper geser, konten, dan panel yang ditampilkan

Tempatkan konten latar depan terlebih dahulu dan kontrol yang ditampilkan (tombol tindakan atau menu samping) kedua di dalam penampung scroll .SwipeReveal. Karena konten berada di urutan pertama dalam DOM, penampung scroll secara alami dimulai pada offset scroll 0 dengan panel yang disembunyikan di luar layar di sebelah kanan.

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

Langkah 2: Konfigurasikan penampung snap horizontal dan overscroll-behavior: chain

Saat Anda menggabungkan elemen dalam penampung scroll luar hanya untuk membuat efek geser untuk menampilkan, wrapper luar tersebut menjadi scroller perantara di dalam dokumen (atau penampung scroll induk lainnya).

Empat kata kunci overscroll-behavior secara independen mengontrol apakah scroll berlebih akan diteruskan ke penampung scroll ancestor (perantaian scroll) dan apakah penampung menampilkan affordance overscroll lokal seperti peregangan karet elastis atau glow tepi (efek batas lokal):

Nilai Scroll Chaining (Menyebar ke Elemen Induk) Efek Batas Lokal (Karet Gelang/Cahaya) Peran dalam Geser untuk Mengungkapkan
auto Ya Ya Perilaku default; pertahankan ini pada scroller konten dalam yang menginginkan masukan pantulan normal, tetapi hindari pada wrapper geser luar yang menyebabkan menu memantul
chain Ya Tidak Gunakan pada scroller .SwipeReveal luar sehingga scroll berlebih akan dirantai ke scroller induk (seperti dokumen) tanpa menu atau strip tindakan memantul
contain Tidak Ya Menjebak scroll di dalam penampung sambil tetap memantul secara lokal
none Tidak Tidak Memblokir pantulan lokal dan perantaian scroll ke ancestor

Buat .SwipeReveal menjadi petak dua kolom (grid-template-columns: 100% max-content) dengan .SwipeReveal-content diposisikan ke start dan .SwipeReveal-panel diposisikan ke 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;
  }
}

Jika Anda juga ingin menampilkan kontrol di sisi kiri (inline-start) menggunakan tiga kolom (grid-template-columns: max-content 100% max-content) atau dua kolom (max-content 100%), tempatkan .SwipeReveal-panel di awal sebelum .SwipeReveal-content. Karena .SwipeReveal-content memiliki scroll-initial-target: nearest, penampung scroll awalnya berada di kolom konten, bukan panel utama.

Langkah 3: Posisikan panel konten yang dapat difokuskan kembali ke tampilan saat fokus keyboard

Karena tombol atau link di dalam .SwipeReveal-panel berada di luar layar saat tidak digunakan, menekan tombol tab untuk mengaksesnya dengan keyboard akan otomatis men-scroll .SwipeReveal untuk menampilkan kontrol yang difokuskan. Namun, karena .SwipeReveal-content mencakup 100% lebar penampung luar, sebagian besar tetap berada di dalam scrollport meskipun panel samping terbuka—jadi menekan Shift+Tab untuk memindahkan fokus kembali ke .SwipeReveal-content (atau ke elemen yang dapat difokuskan di dalamnya) tidak akan otomatis men-scroll trek kembali ke 0.

Proses focusin di .SwipeReveal-content dan periksa event.target.matches(':focus-visible') agar fokus keyboard pada panel atau turunan apa pun mengembalikan .SwipeReveal-content ke awal tanpa mengganggu klik pointer atau tarik sentuh:

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

Praktik terbaik dan kesalahan umum

  • LAKUKAN: gunakan scroll-snap-type: x mandatory, bukan proximity. Dengan proximity, geser ringan dapat membuat tombol tindakan atau menu samping terekspos sebagian saat tidak digunakan.
  • LAKUKAN setelan overscroll-behavior: chain pada scroller geser horizontal luar untuk menampilkan (.SwipeReveal) sehingga scroll dapat dihubungkan ke scroller induk tanpa menu atau strip tindakan yang ditampilkan memantul.
  • LAKUKAN biarkan overscroll-behavior: auto (default) di panel konten dalam yang dapat di-scroll (.SwipeReveal-content.is-scrollable) tempat Anda ingin masukan pantulan normal terjadi sambil tetap merantai geser horizontal ke scroller geser luar.
  • LAKUKAN penerapan tindakan yang ditampilkan dan item menu samping sebagai kontrol DOM yang dapat difokuskan secara nyata (<button>, <a>). Jika .SwipeReveal-content dapat difokuskan atau berisi kontrol yang dapat difokuskan, pasangkan pemroses focusin dengan event.target.matches(':focus-visible') untuk memanggil scrollIntoView({ block: 'nearest', inline: 'start' }) sehingga menekan tab kembali ke panel konten akan menutup panel samping tanpa memengaruhi interaksi pointer.
  • JANGAN mengintersepsi peristiwa wheel, touchmove, atau pointermove di JavaScript untuk menerjemahkan konten secara manual atau mengemulasikan perantaian scroll. Intersepsi gestur thread utama memblokir scroll yang didorong compositor, merusak momentum scroll native, dan menurunkan responsivitas.

Pengganti

Jika penampung .SwipeReveal menempatkan .SwipeReveal-panel di sisi kiri (inline-start) sebelum .SwipeReveal-content, browser yang tidak mendukung scroll-initial-target: nearest akan merender di offset scroll 0 dengan panel utama terekspos.

Jika target Dasar Pengukuran Anda tidak mendukung scroll-initial-target, deteksi dukungan dengan CSS.supports('scroll-initial-target', 'nearest') dan panggil scrollTo() dengan behavior: 'instant' di load sehingga penampung dengan panel utama langsung melompat ke .SwipeReveal-content tanpa men-scroll halaman:

Progressive enhancement

Perlakukan overscroll-behavior: chain dan scrollbar-width: none sebagai progressive enhancement tanpa penggantian JavaScript:

  • Penggantian cascade CSS otomatis: Nilai awal overscroll-behavior adalah auto. Browser yang belum mengenali kata kunci chain akan mengabaikan overscroll-behavior: chain pada waktu penguraian dan mempertahankan auto. Karena auto sudah mengizinkan perangkaian scroll ke penampung scroll ancestor, gestur geser untuk menampilkan dan perangkaian scroll ke induk berfungsi di semua browser secara langsung; browser yang mendukung cukup menyembunyikan pantulan karet yang tidak diinginkan pada wrapper geser luar.
  • Mengganti contain atau none: Jika stylesheet dasar atau class utilitas menetapkan overscroll-behavior: contain atau none, deklarasikan overscroll-behavior: auto tepat sebelum overscroll-behavior: chain sehingga browser yang belum mendukung chain masih merantai scroll ke induk, bukan menjebaknya.
  • Menyembunyikan scrollbar (scrollbar-width: none): Menyembunyikan jalur scrollbar horizontal murni bersifat kosmetik; jika mesin yang lebih lama mengabaikan scrollbar-width: none, penampung geser untuk menampilkan tetap berfungsi sepenuhnya dengan scrollbar standar.