תבניות של החלקה לחשיפה מאפשרות למשתמשים להחליק שורה בחזית או חלונית תוכן באופן אופקי כדי לחשוף ממשק משתמש מוסתר, כמו לחצני פעולה לפי הקשר (הצמדה, השתקה, העברה לארכיון, מחיקה) או תפריט צד משני, בלי להפעיל פעולה כלשהי באופן אוטומטי. במקום לבצע פעולה במהלך ההחלקה, הקונטיינר נפתח במהירות כדי לחשוף פקדים אינטראקטיביים שהמשתמש יכול לבדוק ולהפעיל, או להחליק חזרה כדי לסגור.
כדי להגדיר תנועת החלקה ברוחב מלא שמאשרת ומסירה פריט באופן מיידי אחרי שעוברים את הסף, אפשר לעיין במאמר החלקה להסרה. כדי לחשוף כותרת מוסתרת בחלק העליון של קונטיינר גלילה אנכי, ראו משיכה לחשיפה.
איך מטמיעים
לא משנה אם אתם חושפים כפתורי פעולה בפריט ברשימה או בתפריט צדדי ליד חלונית שאפשר לגלול, המכניקה זהה: עוטפים את התוכן בחזית ואת החלונית הנסתרת בתוך מאגר גלילה אופקי חיצוני שמוגדר עם CSS Scroll Snap (scroll-snap-type: x mandatory) ו-overscroll-behavior: chain. מנוע הגלילה המקורי של הדפדפן מטפל בתנופה, בהצמדה, בגלילה של מיקוד המקלדת ובשרשור גלילה ללא מאזינים לתנועות ב-JavaScript.
שלב 1: מסמנים את רכיב ה-wrapper של ההחלקה, את התוכן ואת החלונית שנחשפת
ממקמים את התוכן בחזית קודם, ואת הפקדים שמוצגים (כפתורי פעולה או תפריט צדדי) אחריו בתוך מאגר הגלילה .SwipeReveal. מכיוון שהתוכן הוא הראשון בסדר ה-DOM, מאגר הגלילה מתחיל באופן טבעי בהיסט הגלילה 0 כשהחלונית מוסתרת מחוץ למסך בצד שמאל.
<!-- 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>
שלב 2: מגדירים את הקונטיינר האופקי ואת overscroll-behavior: chain
כשעוטפים רכיב במאגר גלילה חיצוני רק כדי ליצור אפקט של החלקה לחשיפה, המאגר החיצוני הופך לרכיב גלילה ביניים בתוך המסמך (או מאגר גלילה הורה אחר).
ארבע מילות המפתח של overscroll-behavior קובעות באופן עצמאי אם גלילה מעבר לקצה מועברת למאגר גלילה אב (שרשור גלילה) ואם המאגר מציג אפשרויות גלילה מקומיות כמו מתיחה אלסטית של רצועת גומי או זוהר בקצה (אפקט גבול מקומי):
| ערך | שרשור גלילה (מופץ לאב הקדמון) | אפקט גבול מקומי (גומי / זוהר) | תפקיד ב"החלקה לחשיפה" |
|---|---|---|---|
auto |
כן | כן | פעולת ברירת מחדל; צריך להשאיר את האפשרות הזו מופעלת בסרגלי גלילה של תוכן פנימי שבהם רוצים לקבל משוב רגיל על הקפצה, אבל להימנע ממנה במעטפת ההחלקה החיצונית שבה היא גורמת לתפריט להקפיץ |
chain |
כן | לא | שימוש בסרגל הגלילה החיצוני של .SwipeReveal כדי ששרשראות גלילה עודפות יגללו את סרגל הגלילה הראשי (כמו המסמך) בלי שהתפריט או רצועת הפעולות יקפצו |
contain |
לא | כן | הגלילה נלכדת בתוך הקונטיינר, אבל עדיין יש אפקט קפיצה מקומי |
none |
לא | לא | חסימה של הקפצה מקומית ושל שרשור גלילה לאבות קדמונים |
הופכים את .SwipeReveal לרשת עם שתי עמודות (grid-template-columns: 100% max-content) שבה .SwipeReveal-content נצמד אל start ו-.SwipeReveal-panel נצמד אל 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;
}
}
אם רוצים להציג גם את אמצעי הבקרה בצד ימין (
inline-start) באמצעות שלוש עמודות (grid-template-columns: max-content 100% max-content) או שתי עמודות (max-content 100%), צריך להציב את.SwipeReveal-panelלפני.SwipeReveal-content. בגלל שהמאפיין.SwipeReveal-contentשלscroll-initial-target: nearestמוגדר, מאגר הגלילה מתמקד בהתחלה בעמודת התוכן ולא בחלונית הראשית.
שלב 3: החזרת חלוניות תוכן שניתן להתמקד בהן לתצוגה באמצעות מיקוד מקלדת
הלחצנים או הקישורים בתוך .SwipeReveal-panel לא מוצגים במסך כשהם לא בשימוש, ולכן כשמקישים על Tab כדי להגיע אליהם באמצעות מקלדת, מתבצעת גלילה אוטומטית של .SwipeReveal כדי להציג את הפקד שבמיקוד. עם זאת, מכיוון ש-.SwipeReveal-content משתרע על פני 100% מהרוחב של מאגר התגים החיצוני, רובו נשאר בתוך יציאת הגלילה גם כשהחלונית הצדדית פתוחה. לכן, לחיצה על Shift+Tab כדי להחזיר את המיקוד אל .SwipeReveal-content (או אל רכיב שניתן להתמקד בו בתוכו) לא תגלול באופן אוטומטי את הטראק בחזרה אל 0.
מאזינים ל-focusin ב-.SwipeReveal-content ובודקים את event.target.matches(':focus-visible') כדי שמיקוד המקלדת בחלונית או בכל צאצא יחזיר את .SwipeReveal-content להתחלה בלי להפריע לקליקים של הסמן או לגרירות במגע:
for (const content of document.querySelectorAll('.SwipeReveal-content')) {
content.addEventListener('focusin', (event) => {
if (event.target.matches(':focus-visible')) {
content.scrollIntoView({
block: 'nearest',
inline: 'start',
});
}
});
}
שיטות מומלצות וטעויות נפוצות
- מומלץ להשתמש ב-
scroll-snap-type: x mandatoryבמקום ב-proximity. ב-proximity, החלקה קלה יכולה להשאיר את כפתורי הפעולה או את תפריט הצד חשופים חלקית במצב מנוחה. - מומלץ להגדיר את
overscroll-behavior: chainבסרגל הגלילה האופקי החיצוני של החשיפה בהחלקה (.SwipeReveal) כדי שהגלילה תוכל להשתרשר לסרגל הגלילה הראשי בלי שהתפריט או רצועת הפעולות שנחשפו יקפצו. - מומלץ להשאיר את הערך
overscroll-behavior: auto(ברירת המחדל) בחלוניות תוכן פנימיות שניתן לגלול בהן (.SwipeReveal-content.is-scrollable), שבהן רוצים שתהיה תגובת הקפצה רגילה, ועדיין לקשר החלקות אופקיות לסרגל הגלילה החיצוני. - מומלץ להטמיע פעולות שנחשפות ופריטים בתפריט הצדדי כפקדי DOM אמיתיים שאפשר להתמקד בהם (
<button>,<a>). כש-.SwipeReveal-contentהוא פקד שאפשר להתמקד בו או שהוא מכיל פקדים שאפשר להתמקד בהם, צריך להתאים מאזיןfocusinל-event.target.matches(':focus-visible')כדי לקרוא ל-scrollIntoView({ block: 'nearest', inline: 'start' }), כך שמעבר חזרה לחלונית התוכן באמצעות Tab יסגור את החלונית הצדדית בלי להשפיע על האינטראקציות של הסמן. - אין ליירט אירועי
wheel,touchmoveאוpointermoveב-JavaScript כדי לתרגם את התוכן באופן ידני או לדמות שרשור גלילה. יירוט תנועות ב-thread הראשי חוסם גלילה מבוססת-קומפוזיטור, משבש את המומנטום של הגלילה המקורית ופוגע בתגובתיות.
חלופי
כשמאגר .SwipeReveal ממקם .SwipeReveal-panel בצד שמאל (inline-start) לפני .SwipeReveal-content, דפדפנים שלא תומכים ב-scroll-initial-target: nearest יעבדו בהתחלה בהיסט גלילה 0 כשהחלונית הראשית חשופה.
אם יעד ה-Baseline לא תומך ב-scroll-initial-target, צריך לזהות תמיכה באמצעות CSS.supports('scroll-initial-target', 'nearest') ולהפעיל את scrollTo() עם behavior: 'instant' ב-load כדי שקונטיינרים עם חלונית ראשית יעברו ישירות אל .SwipeReveal-content בלי לגלול את הדף:
שיפור הדרגתי (progressive enhancement)
התייחסו אל overscroll-behavior: chain ו-scrollbar-width: none כשיפורים הדרגתיים (progressive enhancements) בלי חלופות JavaScript:
- Automatic CSS cascade fallback: הערך הראשוני של
overscroll-behaviorהואauto. דפדפנים שעדיין לא מזהים את מילת המפתחchainמתעלמים מ-overscroll-behavior: chainבזמן הניתוח ושומרים אתauto. מכיוון ש-autoכבר מאפשר שרשור גלילה למאגרי גלילה קודמים, גם תנועת ההחלקה לחשיפה וגם שרשור הגלילה להורה פועלים בכל הדפדפנים באופן מיידי. דפדפנים תומכים פשוט מבטלים את ההקפצה הלא רצויה של הגומייה במעטפת ההחלקה החיצונית. - שינוי של
containאוnone: אם גיליון סגנונות בסיסי או מחלקת שירות מגדירים אתoverscroll-behavior: containאוnone, צריך להצהיר עלoverscroll-behavior: autoמיד לפניoverscroll-behavior: chainכדי שדפדפנים שעדיין לא תומכים ב-chainעדיין ישרשרו את הגלילה לאלמנט ההורה במקום ללכוד אותה. - הסתרת פס הגלילה (
scrollbar-width: none): הסתרת פס הגלילה האופקי היא למטרות אסתטיות בלבד. אם מנוע ישן יותר מתעלם מ-scrollbar-width: none, הקונטיינר של החשיפה בהחלקה נשאר פונקציונלי לחלוטין עם פס גלילה רגיל.