קרוסלת תקצירים של סנאפים

שאילתות של מאפייני קונטיינר למצב גלילה מאפשרות לעצב רכיבים על סמך מצב הגלילה הנוכחי שלהם, למשל אם רכיב "תקוע" (באמצעות מיקום דביק) או "נצמד" (באמצעות גלילה עם נקודות עצירה). כך אפשר להציג קרוסלות או גלריות שבהן אפשר להבחין בפריט הפעיל באופן חזותי, בלי להסתמך על רכיבי IntersectionObserver או על רכיבי event listener של גלילה ב-JavaScript.

הטמעה בסיסית

כדי להדגיש פריטים שמוצמדים, צריך ליצור מאגר של הצמדה בגלילה, להגדיר את יעדי ההצמדה כמאגרים של מצב גלילה, ואז לשלוח שאילתה לגבי המצב הזה כדי לעצב את הצאצאים.

1. הגדרת מאגר הגלילה עם נקודות העצירה (scroll snapping)

במאגר התגים הראשי צריך להפעיל את scroll-snap-type.

<div class="carousel">
  <div class="carousel-item">
    <div class="card">Product 1 content</div>
  </div>
  <div class="carousel-item">
    <div class="card">Product 2 content</div>
  </div>
</div>
.carousel {
  display: flex;
  overflow-x: auto;
  /* MANDATORY: Enable scroll snapping on the container */
  scroll-snap-type: x mandatory;
}

2. הגדרת יעדי הצמדה כמאגרי מצב גלילה

כל פריט בקרוסלה שרוצים לעקוב אחריו לצורך הצמדה צריך להיות מוצהר כמאגר scroll-state.

.carousel-item {
  /* Define where the item snaps within the container */
  scroll-snap-align: center;
  
  /* MANDATORY: Establish this element as a scroll-state query container */
  container-type: scroll-state;
}

3. שליחת שאילתה לגבי המצב של snapped

שאילתות קונטיינר מעצבות צאצאים, לכן צריך להחיל את סגנונות ההדגשה על רכיב בתוך יעד ההצמדה. מכיוון שקונטיינר הגלילה מוגדר לגלישה בציר ה-X, צריך להשתמש בשאילתה scroll-state(snapped: x).

חובה: צריך לעטוף את הסגנונות ב-@media (prefers-reduced-motion: no-preference) כדי להציג את האפקט רק למשתמשים שלא ביקשו להפחית את התנועה. בהתאם לתרחיש לדוגמה, יכול להיות שחלקים מהאפקט יישמרו, אבל במקרה הזה, הכרטיסים מהבהבים מלבן לכחול באופן שעלול לגרום לבעיות אצל חלק מהמשתמשים, ולכן אנחנו משביתים אותו לחלוטין.

/* Specify transition outside of queries so that it is applied regardless of state.  */
.card {
  transition:
    scale 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
    background-color 0.4s,
    color 0.4s,
    box-shadow 0.4s;
}
/* 
Only show the effect for users not requesting reduced motion. Disable completely, including the color change, as it causes a flash that may be problematic.
*/
@media (prefers-reduced-motion: no-preference) {
  /* Style the content when its parent .carousel-item is snapped on the x axis */
  @container scroll-state(snapped: x) {
    .card {
      background: #007bff;
      color: white;
      scale: 1.15;
      box-shadow: 0 10px 25px rgba(0, 123, 255, 0.3);
    }
  }
}

/* MANDATORY Copy-Paste Safety: Disable highlight scaling/flashing for motion sensitive users */
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none !important;
    scale: 1 !important;
  }
}

המתאר snapped יכול לשלוח שאילתות לצירים ספציפיים: x,‏ y,‏ inline,‏ block או both.

נגישות

AVOID: להשתמש ב-scroll-state עם רכיבים אינטראקטיביים.

הדגשות חזותיות של פריטים שמוצמדים יכולות לשפר את חוויית המשתמש, אבל הפריט המוצמד לא נחשף לעץ הנגישות. העיצוב החזותי של פריט שמוצמד לא צריך להעביר את המסר שהרכיב פעיל או ממוקד, וטבעת מיקוד המקלדת צריכה להיות בולטת מאוד ושונה מההדגשה של snapped. אם הפריט שמוצמד הוא אינטראקטיבי, צריך להשתמש בשיטות נגישות סטנדרטיות אחרות כדי להפוך אותו לנגיש.

ההצמדה מתרחשת בגלל הגלילה, שלא מזיזה את מיקוד המקלדת. עם זאת, מיקוד המקלדת עלול לגרום לקונטיינר הגלילה לזוז, וכתוצאה מכך לשנות את הפריט שהוצמד, שיכול להיות הפריט שבמיקוד או לא. זה עלול לבלבל את המשתמשים, ולכן לא מומלץ לעשות את זה.

אסטרטגיות חלופיות

בדפדפנים שלא תומכים בשאילתות של מצב גלילה, צריך לספק חוויית בסיס פונקציונלית שבה כל הפריטים קריאים, גם בלי ההדגשה "פעיל".

זיהוי תכונות

אתם יכולים להשתמש ב-@supports כדי לספק שיפורים רק לדפדפנים נתמכים:

@supports (container-type: scroll-state) {
  /* Enhancement styles here */
}

חלופה ל-JavaScript

אם ההדגשה קריטית לחוויית המשתמש, אפשר להשתמש ב-IntersectionObserver כדי לקבוע את הפריט שהוצמד. כדי להתאים את האזור הנצפה לפרוסה דקה במרכז הקרוסלה, צריך לספק rootMargin עם ערך שלילי בתוך השורה. לדוגמה, כדי שרכיב ייחשב כחופף אם הוא נמצא ב-2% המרכזיים של הקרוסלה, מגדירים את rootMargin ל-"0px -49%".

// Optional: detect support and apply a JS-based fallback
if (!CSS.supports('container-type', 'scroll-state')) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      // Toggle a class based on intersection
      entry.target.classList.toggle('is-snapped', entry.isIntersecting);
    });
  }, {
    root: document.querySelector(".carousel"),
    // Carousel item intersects if any part of the carousel item is in the middle 2% of the carousel.
    rootMargin: "0px -49%"
  });

  document.querySelectorAll('.carousel-item').forEach(item => {
    observer.observe(item);
  });
}