मोबाइल ऐप्लिकेशन में, स्वाइप करके हटाने वाले पैटर्न आम तौर पर इस्तेमाल किए जाते हैं. हालांकि, वेब पर इन्हें सही तरीके से लागू करना मुश्किल हो सकता है. सीएसएस स्क्रोल स्नैप का इस्तेमाल करके, स्वाइप करने का एक आसान और नेटिव अनुभव बनाया जा सकता है. यह सीधे ब्राउज़र के स्क्रोलिंग इंजन से जुड़ता है. इससे बेहतरीन परफ़ॉर्मेंस और फ़िज़िक्स पर आधारित मोमेंटम मिलता है. इसके लिए, JavaScript जेस्चर की किसी जटिल लाइब्रेरी की ज़रूरत नहीं होती.
एक ही कार्रवाई वाले स्वाइप (हटाएं, संग्रहित करें, पढ़ा गया के तौर पर मार्क करें, स्नूज़ करें) के लिए भी यही पैटर्न काम करता है. ऐक्शन विज़ुअल बदल जाते हैं, लेकिन गेम के मैकेनिक्स नहीं बदलते.
अगर आपको हॉरिज़ॉन्टल स्वाइप करके, ऐक्शन बटन का सेट या साइड मेन्यू खोलना है, लेकिन स्वाइप करते समय कोई कार्रवाई अपने-आप नहीं करनी है, तो स्वाइप करके दिखाएं देखें.
इन्हें कैसे लागू करें
इस कॉम्पोनेंट में दो लेयर होती हैं: एक सूची (<ul>) और उसके अंदर मौजूद आइटम. हर आइटम को बाहरी <li>, अंदरूनी स्क्रोल ट्रैक (स्नैप पॉइंट वाला स्क्रोल कंटेनर), और कॉन्टेंट एलिमेंट (दिखने वाली लाइन) के तौर पर स्ट्रक्चर किया जाता है. कार्रवाई का यूज़र इंटरफ़ेस (यूआई), जैसे कि ट्रैश आइकॉन, संग्रह लेबल वगैरह, कॉन्टेंट के दोनों तरफ़ मौजूद होता है. सूची में शेयर की गई वायरिंग (लेज़ी आइटम सेटअप, नए जोड़े गए आइटम चुनना) होती है. हर आइटम में स्वाइप का पता लगाने की अपनी सुविधा होती है.
पहला चरण: ट्रैक और कॉन्टेंट के साथ सूची को मार्क अप करना
<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>
दूसरा चरण: ट्रैक को तीन स्नैप पॉइंट वाले हॉरिज़ॉन्टल स्नैप कंटेनर के तौर पर कॉन्फ़िगर करना
ट्रैक में पूरी चौड़ाई वाले तीन कॉलम होते हैं: बाईं ओर का स्पेसर (::before), कॉन्टेंट, और दाईं ओर का स्पेसर (::after). स्पेसर पर स्नैप करने का मतलब है कि कॉन्टेंट पूरी तरह से ऑफ़-स्क्रीन है. यह स्वाइप करने के बाद की स्थिति होती है.
ट्रैक कॉन्फ़िगरेशन, सूची के आइटम पर .is-initialized क्लास के पीछे छिपा होता है. JS के ज़रिए लाइन को अपग्रेड करने से पहले, आइटम सिर्फ़ सामान्य कॉन्टेंट के तौर पर रेंडर होता है. इसमें हॉरिज़ॉन्टल स्क्रोलर नहीं होता. स्वाइप का पता लगाने वाली JavaScript को जोड़ने के बाद, क्लास को चौथे चरण में जोड़ा जाता है. इससे यह पक्का होता है कि फ़ंक्शन के तैयार होने से पहले, उपयोगकर्ता स्वाइप न कर पाए.
.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;
}
तीसरा चरण: सूची के आइटम में ऐक्शन आइकॉन जोड़ना
ऐक्शन कलर, स्पेसर पर मौजूद होता है (दूसरा चरण). ऐक्शन आइकॉन, सूची के आइटम पर ही मौजूद होता है. यह लाइन के बाएं और दाएं किनारों पर ऐंकर किया जाता है.
नीचे दिए गए प्लेसमेंट, साइज़, और मोशन, शुरुआती सुझाव हैं, ज़रूरी नहीं है कि आप इन्हीं के हिसाब से डिवाइस चुनें. अपने डिज़ाइन के हिसाब से, आइकॉन का साइज़, एज इनसेट, थ्रेशोल्ड-पॉप स्केल, ट्रांज़िशन की अवधि, और स्यूडो-एलिमेंट बनाम रीयल डीओएम नोड का विकल्प अडजस्ट करें. इसके लिए, सिर्फ़ यह ज़रूरी है कि आइकॉन, कॉन्टेंट के पीछे हो, ताकि स्वाइप करने से पहले कॉन्टेंट उसे कवर कर सके. साथ ही, आइकॉन सूची के आइटम के अंदर हो, ताकि वह स्पेसर के साथ स्क्रोल न हो. बाकी सब कुछ स्वाद पर निर्भर करता है.
/* 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;
}
चौथा चरण: IntersectionObserver की मदद से, कमिट जेस्चर का पता लगाना
ट्रैक पर मौजूद IntersectionObserver का इस्तेमाल करें और कॉन्टेंट को देखें. जब उपयोगकर्ता स्वाइप करता है, तो ट्रैक के साथ कॉन्टेंट का इंटरसेक्शन रेशियो कम हो जाता है. हम दो थ्रेशोल्ड का इस्तेमाल करते हैं:
activateThreshold: ज़्यादा अनुपात (जैसे, 0.8). जब कॉन्टेंट का दिखने वाला हिस्सा इससे कम हो जाता है, तो उपयोगकर्ता विज़ुअल ऐक्टिवेट पॉइंट को पार कर जाता है. आइकॉन-पॉप अफ़ोर्डेंस को टॉगल करें.commitThreshold: कम अनुपात (जैसे, 0.2). जब दिखने वाला हिस्सा इससे कम हो जाता है, तो इसका मतलब है कि उपयोगकर्ता ने फ़ॉर्म सबमिट कर दिया है. स्नैप जेस्चर के पूरी तरह से सेटल होने का इंतज़ार किए बिना, हटाने वाले ऐनिमेशन को तुरंत शुरू करें. छोटा होने वाली लाइन, उपयोगकर्ता के स्वाइप करने की स्पीड के साथ तालमेल बिठाती है. इससे, स्नैप के लैंड होने का इंतज़ार करने के बजाय, तुरंत प्रतिक्रिया मिलती है.
यहां दो और समस्याओं को हल किया गया है:
- हर आइटम के लिए लेज़ी सेटअप: व्यूपोर्ट में मौजूद एक बाहरी
IntersectionObserver, सेटअप को कंट्रोल करता है. साथ ही, अंदरूनी स्वाइप ऑब्ज़र्वर को शुरू/बंद करता है. आइटम सिर्फ़ तब वायर किए जाते हैं, जब वे पहली बार स्क्रोल करके व्यू में आते हैं. साथ ही, स्क्रीन से बाहर स्क्रोल होने वाले आइटम के लिए, स्वाइप ऑब्ज़र्वर को रोक दिया जाता है. इससे ऐक्टिव ऑब्ज़र्वर की संख्या सीमित रहती है. साथ ही, आइटम के रेंडर होने से पहले, लेआउट पर निर्भर वैल्यू (जैसे,clientWidth) को पढ़ने से बचा जा सकता है. - डाइनैमिक आइटम: असल सूचियां समय के साथ बढ़ती हैं (शुरुआती रेंडर, इनफ़ाइनाइट स्क्रोलिंग, सर्वर पुश).
<ul>पर मौजूदMutationObserver, जोड़े गए किसी भी नए आइटम को बाहरी ऑब्ज़र्वर के साथ रजिस्टर करता है.
// 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);
पांचवां चरण: अपने इस्तेमाल के हिसाब से कार्रवाई और लेबल का इस्तेमाल करना
हटाने के अलावा अन्य वैरिएंट के लिए, सिर्फ़ विज़ुअल और कमिट हैंडलर का मुख्य हिस्सा बदलता है:
- संग्रहित करें: हरे/नीले रंग का बैकग्राउंड, संग्रहित करने का आइकॉन, आइटम को हटाने के बजाय संग्रहित की गई सूची में ले जाएं.
- 'पढ़ा गया' के तौर पर मार्क करें: बैकग्राउंड का रंग हल्का करें, सही के निशान वाला आइकॉन दिखाएं, आइटम की स्थिति अपडेट करें, और फिर से रेंडर करें (या सिर्फ़
.unreadक्लास हटाएं). - स्नूज़ करें: नीला बैकग्राउंड, घड़ी का आइकॉन, चुने गए समय तक छिपाएं.
स्क्रोल/स्नैप/ऑब्ज़र्वेशन के तरीके में कोई बदलाव नहीं किया गया है.
स्वाइप करने की दिशा के हिसाब से अलग-अलग कार्रवाइयां
एक पंक्ति में दो अलग-अलग कार्रवाइयां भी दिख सकती हैं: एक बाईं ओर स्वाइप करने के लिए और दूसरी दाईं ओर स्वाइप करने के लिए (उदाहरण के लिए, दाईं ओर "संग्रहित करें", बाईं ओर "मिटाएं"). कई नेटिव मेल ऐप्लिकेशन ऐसा करते हैं. स्क्रोल, स्नैप, और कमिट-डिटेक्शन के तरीके में कोई बदलाव नहीं होता है. सिर्फ़ कमिट के समय होने वाली प्रोसेस में बदलाव होता है.
चौथे चरण में, कमिट ब्रांच हमेशा removeItem(...) को कॉल करती है. दो कार्रवाइयां करने के लिए, direction के आधार पर हैंडलर चुनें:
if (ratio < commitThreshold) {
const handler = direction === 'left' ? archiveItem : removeItem;
handler(item, content, direction, entry);
viewportObserver.unobserve(item);
observer.disconnect();
return;
}
नाम रखने के बारे में एक नोट: removeItem का नाम, नुकसान पहुंचाने वाले केस के लिए रखा गया है. हालांकि, यह जो फ़ंक्शन चलाता है (लाइन की ऊंचाई को छोटा करना, कॉन्टेंट को स्क्रीन से हटाना, और फिर नोड को छोड़ना) वह असल में एक सामान्य "यह लाइन पूरी हो गई है, इसे ऐनिमेट करके हटाएं" रूटीन है. यह सुविधा, संग्रहित करने, पढ़े गए के तौर पर मार्क करने या स्नूज़ करने के लिए भी काम करती है. दोनों ही मामलों में, पंक्ति इस सूची से हट जाती है. अगर आपके हैंडलर असल में कुछ भी नहीं हटाते हैं (उदाहरण के लिए, दोनों आइटम को कहीं और ले जाते हैं), तो इसका नाम बदलकर dismissItem जैसा कुछ न्यूट्रल रखें, ताकि कोड सही तरीके से पढ़ा जा सके.
दोनों कार्रवाइयों को विज़ुअल तौर पर अलग-अलग दिखाने के लिए, सूची के आइटम पर हर दिशा के लिए एक रंग और आइकॉन जोड़ें. इसके बाद, ट्रैक को स्प्लिट ग्रेडिएंट और एक ही वैरिएबल के दो सूडो-एलिमेंट आइकॉन से पेंट करें.
.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); }
इस सेटअप की मदद से, स्पेसर को अब अपने बैकग्राउंड-कलर की ज़रूरत नहीं होगी. ऐसा इसलिए, क्योंकि ट्रैक का ग्रेडिएंट, रिवील को हैंडल करता है. इसलिए, अगर इस ड्यूअल-ऐक्शन वैरिएंट का इस्तेमाल किया जा रहा है, तो दूसरे चरण से .SwipeableList-track::before, ::after पर background-color नियम को हटाया जा सकता है.
सबसे सही तरीके और नुकसान
- DO use
mandatorysnap, notproximity.proximityकी मदद से, पंक्ति को कुछ हद तक स्क्रोल किया जा सकता है. इससे, कार्रवाई का बैकग्राउंड आधा दिखता है. overscroll-behavior-x: noneको ट्रैक पर सेट करें. इसके बिना, iOS/Android पर ज़्यादा स्वाइप करने से, ब्राउज़र का पीछे जाने वाला जेस्चर ट्रिगर हो सकता है.- कॉन्टेंट के पूरी तरह से ऑफ़-स्क्रीन होने का इंतज़ार करने के बजाय, स्नैप के सेटल होने से पहले, थ्रेशोल्ड पर कमिट करें (उदाहरण के लिए,
commitThreshold ≈ 0.2). इससे जेस्चर के दौरान, हटाने वाला ऐनिमेशन शुरू हो जाता है. यह स्नैप के लैंड होने का इंतज़ार करने से ज़्यादा रिस्पॉन्सिव लगता है. - क्या करें: पेज लोड होने पर हर आइटम को वायर करने के बजाय, बाहरी व्यूपोर्ट
IntersectionObserverसे हर आइटम का सेटअप करें. इससे आइटम रेंडर होने से पहले, लेआउट पर निर्भर वैल्यू (clientWidthवगैरह) को पढ़ने से बचा जा सकता है. साथ ही, ऐक्टिव ऑब्ज़र्वर की संख्या, उपयोगकर्ता को असल में दिखने वाले आइटम के अनुपात में बनी रहती है. - जब आइटम डाइनैमिक तौर पर जोड़े जाते हैं, तो सूची में
MutationObserverका इस्तेमाल ज़रूर करें. जैसे, डेटा लोड होने के बाद शुरुआती रेंडर, इनफ़ाइनाइट स्क्रोल, और सर्वर पुश. इसके बिना, पेज लोड होने के बाद जोड़े गए आइटम वायर नहीं होंगे. - मैन्युअल ट्रांसफ़ॉर्मेशन के लिए,
pointerdown/pointermove/pointerupपर भरोसा न करें. आपको ब्राउज़र से मिलने वाली ये सुविधाएं नहीं मिलेंगी: मोमेंटम, स्नैप-बैक, कीबोर्ड सुलभता, और कम मोशन वाली हैंडलिंग. ये सुविधाएं बिना किसी शुल्क के मिलती हैं. - ज़रूरत पड़ने पर, डेटा मिटाने वाली कार्रवाइयों की पुष्टि ज़रूर करें. "हटाएं" के लिए, स्वाइप पूरा होने के बाद, 'पहले जैसा करें' टोस्ट दिखाने पर विचार करें. जेस्चर तेज़ होता है और गलती से ट्रिगर हो सकता है.
- ज़रूर पक्का करें कि स्क्रोल ट्रैक पर फ़ोकस किया जा सके, उसे कीबोर्ड से ऐक्सेस किया जा सके, और उस पर विज़ुअल फ़ोकस किया जा सके.
- स्वाइप करने पर ट्रिगर होने वाली किसी भी ज़रूरी कार्रवाई के लिए, सुलभता से जुड़े विकल्प ज़रूर उपलब्ध कराएं. जैसे, साफ़ तौर पर दिखने वाला बटन, कॉन्टेक्स्ट मेन्यू या बदलाव करने वाला मोड.
फ़ॉलबैक रणनीतियां
इस्तेमाल की जाने वाली सभी नई सुविधाएं, या तो अनुभव के लिए ज़रूरी नहीं हैं या उनमें मज़बूत फ़ॉलबैक हैं जिनका इस्तेमाल अब भरोसेमंद तरीके से किया जा सकता है.
overscroll-behavior के लिए फ़ॉलबैक
इस इस्तेमाल के उदाहरण के लिए, फ़ॉलबैक की ज़रूरत नहीं है. हालांकि, overscroll-behavior में इंटरऑपरेबिलिटी की एक समस्या है, जो स्क्रोल न किए जा सकने वाले ओवरफ़्लो के बिना कंटेनर पर दिखती है. हालांकि, यहां ट्रैक को हमेशा हॉरिज़ॉन्टल तौर पर स्क्रोल किया जा सकता है (100% चौड़ाई वाले कंटेनर के अंदर तीन पूरी चौड़ाई वाले कॉलम). इसलिए, प्रॉपर्टी सभी ब्राउज़र पर एक जैसा व्यवहार करती है और स्वाइप जेस्चर को भरोसेमंद तरीके से शामिल किया जाता है.
scrollbar-width के लिए फ़ॉलबैक
छिपे हुए स्क्रोलबार, विज़ुअल को बेहतर बनाने के लिए होते हैं. ये स्वाइप करके हटाने की सुविधा को चालू नहीं करते. अगर आपके बेसलाइन टारगेट में scrollbar-width शामिल नहीं है, तो भी लाइन स्क्रोल होती है, स्नैप होती है, कमिट का पता लगाती है, और सही तरीके से हटती है. हालांकि, हो सकता है कि काम न करने वाले अनुभव में सिर्फ़ हॉरिज़ॉन्टल स्क्रोलबार दिखे. अगर आपके प्रॉडक्ट को WebKit पर आधारित पुराने ब्राउज़र में स्क्रोलबार छिपाने की ज़रूरत है, तो स्वाइप ट्रैक के लिए, सीमित दायरे वाला ::-webkit-scrollbar { display: none; } नियम जोड़ा जा सकता है.
scroll-initial-target के लिए फ़ॉलबैक
अगर आपके बेसलाइन टारगेट में scroll-initial-target शामिल नहीं है, तो setupItem में प्रोग्राम के हिसाब से ट्रैक को कॉन्टेंट तक स्क्रोल करें. CSS.supports की मदद से पता लगाएं:
// 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.
}
कॉल ऑर्डर करने से फ़र्क़ पड़ता है. प्रोग्राम के हिसाब से स्क्रोल करने की सुविधा को इस तरह से काम करना चाहिए:
- AFTER
.is-initializedको सूची के आइटम में जोड़ा जाता है. क्लास की मदद से, ट्रैक को स्क्रोल कंटेनर में बदला जाता है. स्क्रोल न किए जा सकने वाले एलिमेंट परscrollLeftसेट करने से कोई कार्रवाई नहीं होती. IntersectionObserverको चौथे चरण में बताए गए तरीके से अटैच करने से पहले. ऐसा न करने पर, बाईं ओर मौजूद स्पेसर के बाद प्रोग्राम के हिसाब से स्क्रोल करने पर, उसे "स्वाइप" के तौर पर देखा जाएगा. साथ ही, कमिट हैंडलर को तुरंत ट्रिगर किया जाएगा.
बाहरी व्यूपोर्ट ऑब्ज़र्वर (चौथा चरण) से सेटअप को चलाना, इसे भरोसेमंद बनाता है: आइटम रेंडर होने के बाद setupItem चलता है. इसलिए, track.clientWidth, 0 के बजाय असल वैल्यू दिखाता है.
कुछ ब्राउज़र (खास तौर पर Safari), ट्रैक का साइज़ बदलने पर स्नैप-कंटेनर की स्क्रोल पोज़िशन को भी रीसेट कर देते हैं. जैसे, यूआरएल-बार को दिखाना/छिपाना, व्यूपोर्ट का साइज़ बदलना, कंटेनर क्वेरी वगैरह. स्क्रोल को फिर से लागू करने के लिए, हर ट्रैक पर ResizeObserver का इस्तेमाल करें. इसे उसी CSS.supports जांच के पीछे गेट करें — जब scroll-initial-target काम करता है, तो ब्राउज़र साइज़ बदलने के समय स्क्रोल को वापस लाने की प्रोसेस को खुद मैनेज करता है.
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.
}
removeItem में लाइन की ऊंचाई का ऐनिमेशन चलने से पहले, ट्रैक को अनऑब्ज़र्व करें. ऐसा न करने पर, ऊंचाई में बदलाव होने से, हटाने के दौरान साइज़ बदलने का कॉलबैक फिर से ट्रिगर हो जाता है. इसे कमिट ब्रांच में मौजूद viewportObserver.unobserve(item) के साथ जोड़ें:
if (ratio < commitThreshold) {
removeItem(item, content, direction, entry);
viewportObserver.unobserve(item);
if (needsScrollWorkaround) trackResizeObserver.unobserve(track);
observer.disconnect();
return;
}