Kald. için kaydır.

Kaydırarak kaldırma kalıpları mobil uygulamalarda yaygındır ancak web'de düzgün bir şekilde uygulanması zor olabilir. CSS kaydırarak tutturma özelliğini kullanarak doğrudan tarayıcının kaydırma motoruna bağlanan, sorunsuz ve doğal bir kaydırma etkileşimi oluşturabilirsiniz. Bu sayede, karmaşık bir JavaScript hareket kitaplığına ihtiyaç duymadan yüksek performans ve fizik tabanlı momentum elde edilir.

Aynı kalıp, tek işlemle kaydırma (kaldırma, arşivleme, okundu olarak işaretleme, erteleme) için de geçerlidir. Eylem görselleri değişir, mekanikler değişmez.

Yatay kaydırma hareketiyle, kaydırma sırasında otomatik olarak bir işlem yapmadan bir işlem düğmesi grubunu veya yan menüyü açmak istiyorsanız Kaydırarak gösterme bölümüne bakın.

Nasıl uygulanır?

Bileşenin iki katmanı vardır: liste (<ul>) ve içindeki öğeler. Her öğe, dış <li>, iç kaydırma track (yakalama noktalarına sahip kaydırma kapsayıcısı) ve content öğesi (görünür satır) olarak yapılandırılır. İşlemin gösterilen kullanıcı arayüzü (çöp kutusu simgesi, arşiv etiketi vb.) içeriğin her iki tarafında da bulunur. Liste, paylaşılan kablolamaya sahiptir (gecikmeli öğe kurulumu, yeni eklenen öğeleri alma); her öğe kendi kaydırma algılamasına sahiptir.

1. adım: Listeyi parça ve içerikle işaretleyin

<ul class="SwipeableList">
  <li id="list-item-1" class="SwipeableList-item">
    <div class="SwipeableList-track">
      <div class="SwipeableList-content">Item One</div>
    </div>
  </li>
  <li id="list-item-2" class="SwipeableList-item">
    <div class="SwipeableList-track">
      <div class="SwipeableList-content">Item Two</div>
    </div>
  </li>
  <!-- ...more items... -->
</ul>

2. adım: Parçayı üç yakalama noktası olan yatay bir yakalama kapsayıcısı olarak yapılandırın

Kanalda üç tam genişlikte sütun vardır: sol boşluk (::before), içerik ve sağ boşluk (::after). Bir boşluğa tutturma, içeriğin tamamen ekran dışında olduğu anlamına gelir. Bu, kaydırma işlemi tamamlandıktan sonraki bekleme konumudur.

Parça yapılandırması, liste öğesindeki bir .is-initialized sınıfı tarafından engelleniyor. JS satırı yükseltmeden önce öğe, yatay kaydırma çubuğu olmadan düz içerik olarak oluşturulur. Kaydırma hareketlerini algılayan JavaScript bağlandıktan sonra sınıf 4. adımda eklenir. Bu sayede, işlev hazır olmadan önce kullanıcının kaydırma yapması engellenir.

.SwipeableList {
  list-style: none;
}

.SwipeableList-item {
  /* Establishes a containing block for the absolutely positioned action
     icons (Step 3) and clips overflow during the row's removal
     animation (Step 4). */
  contain: content;
}

/* The track only becomes a scroll snap container after JS upgrades
   the row by adding `.is-initialized` (see Step 4). */
.SwipeableList-item.is-initialized .SwipeableList-track {
  /* Three full-width columns: left spacer | content | right spacer.
     Width is 100% of the track, so each column fills the viewport row. */
  display: grid;
  grid-template-columns: 100% 100% 100%;

  /* Horizontal scroll only; vertical overflow is clipped so the
     reveal stays inside the row. */
  overflow: scroll clip;

  /* Prevent the swipe from chaining into the page scroll or browser
     back-gesture on iOS/Android. */
  overscroll-behavior-x: none;

  /* Hide the scrollbar; the gesture is the affordance. */
  scrollbar-width: none;

  /* `mandatory` ensures the track always rests on a snap point
     (spacer or content), never partially scrolled. */
  scroll-snap-type: x mandatory;
}

/* Spacers act as the left and right snap targets, AND carry the action's
   reveal color. As the user swipes, the colored spacer slides into view,
   which is what the user sees behind the content. */
.SwipeableList-item.is-initialized .SwipeableList-track::before,
.SwipeableList-item.is-initialized .SwipeableList-track::after {
  content: '';

  /* `scroll-snap-align` is required to make this a valid snap target,
     but the specific value (`start`/`center`/`end`) doesn't matter here
     because each snap point spans the full width of the scroll
     container, so all alignments resolve to the same resting position. */
  scroll-snap-align: start;

  /* `hsl(0 65% 50%)` is an example value; pick whatever fits your
     design (red here signals "delete"; use a different color for
     archive, mark-as-read, etc.). */
  background-color: hsl(0 65% 50%);
}

.SwipeableList-content {
  /* The content sits above the action icons (Step 3), so it covers
     them until the user swipes. */
  position: relative;
  z-index: 2;

  /* Required to make the content a valid snap target (its resting
     position). As with the spacers above, the specific value doesn't
     matter because the snap points are full-width. */
  scroll-snap-align: start;

  /* The content must paint over the revealed spacer color. */
  background: Canvas;

  /* Row separator (example value; customize to taste). */
  border-bottom: 1px solid #eee;
}

/* Gate `scroll-initial-target` behind `.is-initialized` so it only
   applies once the track is actually a scroll container. Setting it on
   the content before then would let the property walk up to the nearest
   scrollable ancestor (typically the document) and shift the page's
   initial scroll position to bring this row's content into view. With
   the gate, the rule is only live when the row's own track can satisfy
   it, so the initial scroll happens inside the track as intended. */
.SwipeableList-item.is-initialized .SwipeableList-content {
  scroll-initial-target: nearest;
}

/* The track is the focusable scroll container, but its overflow is clipped
   so a default focus ring on the track itself would be invisible. Project
   the focus affordance onto the content element (which paints above the
   track) using `:focus-visible` on the track. */
.SwipeableList-track:focus-visible .SwipeableList-content {
  outline: auto;
  outline-offset: -2px;
}

3. adım: İşlem simgelerini liste öğesine ekleyin

İşlem rengi, ayırıcılarda (2. adım) bulunur. İşlem simgesi, liste öğesinin kendisinde bulunur ve satırın sol ve sağ kenarlarına sabitlenir.

Aşağıdaki yerleşim, boyutlandırma ve hareket birer başlangıç önerisidir, zorunlu değildir. Simge boyutunu, kenar dolgularını, eşik pop ölçeğini, geçiş süresini ve hatta tasarımınıza uyacak şekilde sözde öğeler ile gerçek DOM düğümleri arasındaki seçimi ayarlayın. Tek mekanik şart, simgenin içeriğin arkasında (böylece içerik, kaydırma öncesinde simgeyi kapatabilir) ve liste öğesinin içinde (böylece ayırıcıyla birlikte kaydırılmaz) yer almasıdır. Diğer her şey zevk meselesi.

/* Action icons painted on the list item. They're absolutely positioned
   inside the row (which is a containing block thanks to `contain: content`
   on `.SwipeableList-item` from Step 2) and sit at z-index 1, so the
   content element (z-index 2) covers them until the user swipes far
   enough. */
.SwipeableList-item.is-initialized::before,
.SwipeableList-item.is-initialized::after {
  /* Inline an SVG as the action icon. Replace this with whatever icon
     fits the action (archive, checkmark, clock, etc.). The `fill='white'`
     is baked into the SVG so it contrasts with the red spacer background;
     adjust if your background color is light. */
  --action-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'><path d='M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3H9zm0 5h2v9H9V8zm4 0h2v9h-2V8z'/></svg>");

  content: '';
  position: absolute;
  z-index: 1;

  /* The size (`width`) and insets (`left`/`right` below) are example
     values, tune them to match your row height and visual weight.
     The icon fills its box via `background-size: contain`, so changing
     `width` resizes it. */
  width: 1.5em; /* example value, adjust to taste */
  aspect-ratio: 1;
  top: 50%;
  translate: 0 -50%;

  /* Smooth transitions for the icon's visual states: the activate-point
     pop (`scale`, see `.is-activating` below) and the removal fade
     (`scale` + `opacity`, see `.is-removing` below). 0.2s is an example
     duration. */
  transition: scale 0.2s ease, opacity 0.2s ease;

  background: var(--action-icon) center / contain no-repeat;
}
/* Inset from the row edge (example values; adjust to match your layout). */
.SwipeableList-item.is-initialized::before { left: 1.5em; }
.SwipeableList-item.is-initialized::after  { right: 1.5em; }

/* Activating pop: scale the icon up when the user is past the visual
   activate point, so the row's affordance feels reactive. Toggled by JS
   in Step 4. */
.SwipeableList-item.is-activating::before,
.SwipeableList-item.is-activating::after {
  scale: 1.333;
}

/* Removal affordance: fade and shrink the icons as the row collapses.
   Driven by the `is-removing` class added in Step 4 at commit time;
   the existing `transition` on the icons animates the change. */
.SwipeableList-item.is-removing::before,
.SwipeableList-item.is-removing::after {
  scale: 0.5;
  opacity: 0;
}

/* Only show the icon on the *leading* side of the swipe; hide the
   trailing-side one. `data-swipe-direction` is set by JS in Step 4. */
.SwipeableList-item.is-activating[data-swipe-direction="left"]::after,
.SwipeableList-item.is-activating[data-swipe-direction="right"]::before {
  visibility: hidden;
}

4. adım: IntersectionObserver ile onaylama hareketini algılama

İçeriği gözlemleyerek parçaya dayalı IntersectionObserver kullanın. Kullanıcı kaydırdıkça içeriğin parçayla kesişim oranı düşer. İki eşik kullanırız:

  • activateThreshold: yüksek bir oran (ör. 0,8). İçeriğin görünür kısmı bu değerin altına düştüğünde kullanıcı görsel etkinleştirme noktasını geçmiş olur. Simge açma özelliğini açın veya kapatın.
  • commitThreshold: Düşük bir oran (ör. 0,2). Görünür kısım bu değerin altına düştüğünde kullanıcı işlemi tamamlamış olur. Kaldırma animasyonunu, tutturma hareketinin tamamen yerleşmesini beklemeden hemen başlatın. Daralan satır, kullanıcının kaydırma hareketine uyum sağlar. Bu sayede, tepki vermeden önce kaydırma işleminin tamamlanmasını beklemek yerine daha hızlı yanıt veren bir deneyim sunulur.

Burada iki endişe daha ele alınmaktadır:

  • Öğe başına geç yükleme kurulumu: Görüntü alanında köklenen tek bir dış IntersectionObserver, kurulumu ve iç kaydırma gözlemcilerinin başlatılmasını/durdurulmasını sağlar. Öğeler yalnızca ilk kez kaydırılarak görünüme girdiklerinde bağlanır ve ekran dışına kaydırılan öğelerin kaydırma gözlemcisi duraklatılır. Bu sayede etkin gözlemci sayısı sınırlı tutulur ve öğe oluşturulmadan önce düzene bağlı değerlerin (ör. clientWidth) okunması önlenir.
  • Dinamik öğeler: Gerçek listeler zaman içinde büyür (ilk oluşturma, sonsuz kaydırma, sunucu push). <ul> üzerindeki bir MutationObserver, yeni eklenen öğeleri dış gözlemciye kaydeder.
// Per-item handles. Populated when an item is first lazily wired up; read by
// the outer viewport observer to start/stop the inner observer as items enter
// and leave the viewport.
const swipeObservers = new WeakMap();

// Outer observer: drives the entire swipe lifecycle off viewport visibility.
// On first entry, lazily wires the item up (`setupItem` reads layout-dependent
// values like `clientWidth`, which return 0 until the item is rendered). After
// setup, starts the inner observer. On exit, stops the inner observer so
// offscreen items don't track scroll positions.
const viewportObserver = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    const item = entry.target;
    if (entry.isIntersecting) {
      const handle = swipeObservers.get(item) ?? setupItem(item);
      handle.observer.observe(handle.content);
    } else {
      const handle = swipeObservers.get(item);
      if (handle) handle.observer.unobserve(handle.content);
    }
  }
});

function setupItem(item) {
  const track = item.querySelector('.SwipeableList-track');
  const content = track.querySelector('.SwipeableList-content');

  // Upgrade the row into "swipeable" mode. This is the gate for all the CSS
  // from Steps 2 and 3 (the track becomes a snap container, the action icons
  // appear). Done *before* the inner observer is attached so the snap
  // container exists by the time intersection callbacks can fire.
  item.classList.add('is-initialized');

  // Tunable thresholds. `activateThreshold` is the visual feedback point
  // (icon pops). `commitThreshold` is the point of no return: once the
  // content is past this point of being off-screen, we commit even if the
  // user releases mid-gesture and the track snaps back. A low value (~0.2)
  // commits before the snap settles, so the remove animation can start
  // during the swipe.
  const activateThreshold = 0.8;
  const commitThreshold = 0.2;

  // One inner observer per item, rooted at the track. Vertical scrolling of
  // the outer list moves root and target together, so the callback only fires
  // for the horizontal swipe.
  const observer = new IntersectionObserver((entries, observer) => {
    const entry = entries.at(-1);
    const ratio = entry.intersectionRatio;

    // Direction the user is swiping toward. A positive offset from the
    // track's left edge means the content has been pulled right (left
    // spacer revealed), so the leading icon is on the left.
    const direction = (entry.boundingClientRect.x - entry.rootBounds.x) > 0
      ? 'left'
      : 'right';

    if (ratio < commitThreshold) {
      // The IO entry's boundingClientRect is the last reliable measurement
      // before the animation starts; reuse it for both the pre-collapse
      // height and the slide-off translate distance.
      removeItem(item, content, direction, entry);
      viewportObserver.unobserve(item);
      observer.disconnect();
      return;
    }

    // Scale up the leading icon while the content is past the activate
    // point; restore it at rest.
    item.classList.toggle('is-activating', ratio < activateThreshold);

    // Hold the previous direction at rest so the icon's exit animation
    // finishes on the side the user was swiping toward.
    if (entry.boundingClientRect.x !== entry.rootBounds.x) {
      item.dataset.swipeDirection = direction;
    }
  }, {
    root: track,
    threshold: [commitThreshold, activateThreshold],
  });

  // Return the handle without starting observation; the outer viewport observer
  // calls `observer.observe(content)` once the item is in view.
  const handle = {observer, content};
  swipeObservers.set(item, handle);
  return handle;
}

async function removeItem(item, content, direction, entry) {
  const opts = { duration: 300, easing: 'ease', fill: 'forwards' };

  const rect = entry.boundingClientRect;
  // Content's pixel offset from the track's left edge.
  const x = rect.x - entry.rootBounds.x;
  // Pixel distance the content needs to travel to be fully out of view.
  const translate = direction === 'left'
    ? rect.width - x
    : -(x + rect.width);

  // Use a combination of CSS transitions (for declarative styles) and
  // WAAPI animations (for computed values) to remove the element,
  // then await the completion of all of them.
  // Note: the content translate animation is important because the
  // height-collapse animation can otherwise finish before the browser's
  // smooth scroll-snap has scrolled the content fully off-screen.
  item.classList.add('is-removing');
  item.animate([{ height: `${rect.height}px` }, { height: '0px' }], opts);
  content.animate([{ translate: `${translate}px` }], opts);
  await Promise.allSettled(
    item.getAnimations({ subtree: true }).map((a) => a.finished),
  );

  // Safari has a scroll-latching bug: removing the node while the swipe
  // gesture's momentum is still resolving causes the next item (which
  // slides up into this one's place) to inherit the scroll and
  // immediately scroll itself off-screen. Detect Safari via
  // `GestureEvent` (a Safari-only API) and defer the actual DOM removal
  // until the gesture has fully settled. The 5s delay is conservative;
  // anything longer than the momentum tail is fine. Mark the row inert
  // so it can't be interacted with during the delay.
  if (globalThis.GestureEvent) {
    item.inert = true;
    setTimeout(() => item.remove(), 5000);
  } else {
    item.remove();
  }
}

function setupList(list) {
  // Observe items already in the list.
  for (const item of list.children) {
    if (item.matches('.SwipeableList-item')) {
      viewportObserver.observe(item);
    }
  }

  // Pick up items added later (initial render after data loads, infinite
  // scroll, server push). Removals don't need MutationObserver handling —
  // the commit branch above already unobserves the item before removing it
  // from the DOM.
  new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE &&
            node.matches('.SwipeableList-item')) {
          viewportObserver.observe(node);
        }
      }
    }
  }).observe(list, { childList: true });
}

document.querySelectorAll('.SwipeableList').forEach(setupList);

5. adım: Kullanım alanınıza uygun işlemi ve etiketi kullanın

Kaldırma dışındaki varyantlarda yalnızca görseller ve commit işleyicinin gövdesi değişir:

  • Arşivle: Yeşil/mavi arka plan, arşiv simgesi, öğeyi kaldırmak yerine arşiv listesine taşıma.
  • Okundu olarak işaretle: Arka planı soluklaştırın, onay işareti simgesi ekleyin, öğe durumunu güncelleyin ve yeniden oluşturun (veya yalnızca .unread sınıfını kaldırın).
  • Ertele: Mavi arka plan, saat simgesi, seçilen zamana kadar gizleme.

Kaydırma/tutturma/gözlem mekaniği değişmez.

Kaydırma yönüne göre farklı işlemler

Tek bir satırda iki farklı işlem de gösterilebilir: biri sola kaydırma, diğeri sağa kaydırma için (ör.sağa kaydırıldığında "arşivle", sola kaydırıldığında "sil"). Bu, birçok yerel posta uygulamasının kullandığı bir yöntemdir. Kaydırma, tutturma ve kaydetme algılama mekanizmaları hiç değişmez. Değişen tek şey, kaydetme sırasında ne olduğudur.

4. adımda, commit dalı her zaman removeItem(...) çağrısı yapar. İki işlemi desteklemek için işleyiciyi direction değerine göre seçin:

if (ratio < commitThreshold) {
  const handler = direction === 'left' ? archiveItem : removeItem;
  handler(item, content, direction, entry);
  viewportObserver.unobserve(item);
  observer.disconnect();
  return;
}

Adlandırma hakkında bir not: removeItem, yıkıcı durum için adlandırılmıştır ancak çalıştırdığı işlev (satırın yüksekliğini daraltma, içeriği ekran dışına kaydırma ve ardından düğümü bırakma) aslında genel bir "bu satır tamamlandı, animasyonla kaldır" rutinidir. Arşivleme, okundu olarak işaretleme veya erteleme için de aynı şekilde çalışır. Satır her iki durumda da bu listeden kaldırılır. İşleyicileriniz aslında hiçbir şeyi kaldırmıyorsa (ör. her ikisi de öğeyi başka bir yere taşıyorsa) kodun doğru şekilde okunması için işleyiciyi dismissItem gibi nötr bir adla yeniden adlandırın.

İki işlemi görsel olarak ayırt etmek için her yön için bir renk ve simgeyi liste öğesine yerleştirin, ardından parçayı bölünmüş bir gradyan ve aynı değişkenlerden iki sözde öğe simgesiyle boyayın.

.SwipeableList-item {
  /* Action color + icon per swipe direction. `--left-*` is revealed
     when the user swipes RIGHT (e.g., archive); `--right-*` is revealed
     when the user swipes LEFT (e.g., delete). */
  --left-action-color: hsl(140 50% 40%);
  --left-action-icon: url("…archive svg…");
  --right-action-color: hsl(0 65% 50%);
  --right-action-icon: url("…trash svg…");
}

.SwipeableList-item.is-initialized .SwipeableList-track {
  /* Split reveal: left half of the scrollable area gets the left-action
     color, right half gets the right-action color. `background-attachment:
     local` makes the gradient's positioning area the scrollable area
     (3x the visible width), so the default size fills it and the 50% hard
     stop lines up exactly with the midpoint of the resting content column.
     The track's color also stays continuous behind the content as it
     translates off, so the leading-direction color shows the whole way. */
  background-image: linear-gradient(
    to right,
    var(--left-action-color) 50%,
    var(--right-action-color) 50%
  );
  background-attachment: local;
}

/* Per-side icons read from the same variables. */
.SwipeableList-item.is-initialized::before { background-image: var(--left-action-icon); }
.SwipeableList-item.is-initialized::after  { background-image: var(--right-action-icon); }

Bu kurulumda, ayırıcıların artık kendi arka plan rengine ihtiyacı yoktur (parçanın gradyanı görünümü yönetir). Bu nedenle, bu çift işlemli varyantı kullanıyorsanız 2. adımdaki .SwipeableList-track::before, ::after üzerinde background-color kuralını bırakabilirsiniz.

En iyi uygulamalar ve tuzaklar

  • YAPIN: proximity değil, mandatory snap kullanın. proximity ile satır kısmen kaydırılmış olarak kalabilir ve işlem arka planı yarı görünür durumda kalabilir.
  • YAPIN: Kanalda overscroll-behavior-x: none değerini ayarlayın. Bu özellik olmadan, aşırı kaydırma iOS/Android'de tarayıcının geri gezinme hareketini tetikleyebilir.
  • İçeriğin tamamen ekran dışına çıkmasını beklemek yerine, tutturma işlemi tamamlanmadan önce bir eşikte (ör. commitThreshold ≈ 0.2) işlemi tamamlayın. Bu sayede, kaldırma animasyonu hareket sırasında başlar ve kaldırma işleminin tamamlanmasını beklemekten çok daha hızlı yanıt verir.
  • Her öğeyi sayfa yüklemesinde bağlamak yerine, öğe bazında kurulumu dış görüntü alanından IntersectionObserver YAPIN. Bu sayede, öğeler oluşturulmadan önce düzene bağlı değerlerin (clientWidth vb.) okunması önlenir ve etkin gözlemci sayısı, kullanıcının gerçekten görebileceği öğelerle orantılı olur.
  • Öğeler dinamik olarak eklendiğinde (veri yüklendikten sonra ilk oluşturma, sonsuz kaydırma, sunucu push) listede MutationObserver KULLANIN. Bu özellik olmadan, sayfa yüklendikten sonra eklenen öğeler bağlanmaz.
  • DO NOT rely on pointerdown/pointermove/pointerup to drive a manual transform. Tarayıcının size ücretsiz olarak sunduğu ivme, geri çekme, klavye erişilebilirliği ve azaltılmış hareket işleme özelliklerini kaybedersiniz.
  • Uygun durumlarda yıkıcı işlemleri onaylayın (DO). "Kaldır" için, kaydırma işlemi tamamlandıktan sonra geri alma bildirimini göstermeyi düşünün. Bu hareket hızlıdır ve yanlışlıkla tetiklenmesi kolaydır.
  • Kaydırma çubuğunun odaklanılabilir ve klavyeyle erişilebilir olduğundan ve görsel odaklanma imkanı sunduğundan EMİN OLUN.
  • Kaydırma hareketiyle tetiklenen tüm alakalı işlemler için erişilebilir alternatifler (ör. görünür bir düğme, bağlam menüsü veya düzenleme modu) SUNUN.

Yedek stratejiler

Kullanılan tüm yeni özellikler, deneyimin temelini oluşturmaz veya şu anda güvenilir bir şekilde kullanılabilecek güçlü yedeklere sahiptir.

overscroll-behavior için yedek

Bu kullanım alanı için yedek gerekmez. overscroll-behavior, kaydırılabilir taşma olmayan kapsayıcılarda ortaya çıkan bir birlikte çalışabilirlik sorununa sahip olsa da buradaki parça her zaman yatay olarak kaydırılabilir (%100 genişlikteki bir kapsayıcı içinde üç tam genişlikte sütun). Bu nedenle, özellik tarayıcılar arasında tutarlı bir şekilde davranır ve kaydırma hareketi güvenilir bir şekilde kapsanır.

scrollbar-width için yedek

Gizli kaydırma çubukları, kaydırarak kaldırma özelliğinin çalışmasını sağlayan mekanizma değil, görsel bir iyileştirmedir. Temel hedefiniz scrollbar-width içermiyorsa satır yine de doğru şekilde kaydırılır, tutturulur, kaydetmeyi algılar ve kaldırılır. Desteklenmeyen deneyimde yalnızca yatay bir kaydırma çubuğu gösterilebilir. Ürününüz, WebKit tabanlı eski tarayıcılarda gizli kaydırma çubukları gerektiriyorsa kaydırma yolu için dar kapsamlı bir ::-webkit-scrollbar { display: none; } kuralı ekleyebilirsiniz.

scroll-initial-target için yedek

Temel hedefiniz scroll-initial-target içermiyorsa parçayı programatik olarak setupItem içindeki içeriğe kaydırın. CSS.supports ile algılama:

// Hoist the feature detect so the conditional `ResizeObserver` below can be
// skipped entirely when the property is supported.
const needsScrollWorkaround = !CSS.supports('scroll-initial-target', 'nearest');

function setupItem(item) {
  // ...existing setup from Step 4...

  if (needsScrollWorkaround) {
    track.scrollLeft = track.clientWidth;
  }

  // ...attach the inner IntersectionObserver, etc.
}

Çağrı sırası önemlidir. Programatik kaydırma şu durumlarda çalıştırılmalıdır:

  1. AFTER .is-initialized, liste öğesine eklenir (sınıf, parçayı kaydırma kapsayıcısına dönüştürür; kaydırılamayan bir öğede scrollLeft ayarı işlem yapmaz).
  2. 4. adımdaki iç IntersectionObserver takılmadan ÖNCE. Aksi takdirde, sol boşluktan sonraki ilk programatik kaydırma "kaydırma" olarak algılanır ve hemen commit işleyiciyi tetikler.

Kurulumu dış görüntü alanı gözlemcisinden (4. adım) yürütmek, bu yöntemi güvenilir kılar: setupItem, öğe oluşturulduktan sonra çalışır. Bu nedenle track.clientWidth, 0 yerine gerçek bir değer döndürür.

Bazı tarayıcılar (özellikle Safari), parça yeniden boyutlandırıldığında (URL çubuğunun gösterilmesi/gizlenmesi, görüntü alanının yeniden boyutlandırılması, kapsayıcı sorguları vb.) yakalama kapsayıcısının kaydırma konumunu da sıfırlar. Kaydırmayı yeniden uygulamak için her parçada ResizeObserver kullanın. Aynı CSS.supports kontrolüyle kısıtlayın. scroll-initial-target desteklendiğinde tarayıcı, yeniden boyutlandırma sırasında kaydırma geri yükleme işlemini kendisi yapar.

const trackResizeObserver = needsScrollWorkaround
  ? new ResizeObserver((entries) => {
      for (const entry of entries) {
        entry.target.scrollLeft = entry.target.clientWidth;
      }
    })
  : null;

function setupItem(item) {
  // ...existing setup...

  if (needsScrollWorkaround) {
    track.scrollLeft = track.clientWidth;
    trackResizeObserver.observe(track);
  }

  // ...attach the inner IntersectionObserver, etc.
}

Satırın yükseklik animasyonu removeItem içinde çalıştırılmadan önce parçayı gözlemlemeyi bırakın. Aksi takdirde, yükseklik değişikliği kaldırma sırasında yeniden boyutlandırma geri çağırma işlevini yeniden tetikler. Bunu, commit dalındaki mevcut viewportObserver.unobserve(item) öğesine ekleyin:

if (ratio < commitThreshold) {
  removeItem(item, content, direction, entry);
  viewportObserver.unobserve(item);
  if (needsScrollWorkaround) trackResizeObserver.unobserve(track);
  observer.disconnect();
  return;
}