Карусель Snap Highlights

Запросы контейнера состояния прокрутки позволяют стилизовать элементы на основе их текущего состояния прокрутки, например, является ли элемент "застрявшим" (через липкое позиционирование) или "привязанным" (через привязку прокрутки). Это позволяет создавать карусели и галереи, в которых активный элемент можно визуально выделить без использования наблюдателей пересечения JavaScript или прослушивателей событий прокрутки.

Базовая реализация

Чтобы выделить привязанные элементы, необходимо создать контейнер scroll-snap, определить целевые объекты привязки как контейнеры scroll-state, а затем запросить это состояние для стилизации дочерних элементов.

1. Создайте контейнер с привязкой прокрутки

В родительском контейнере должен быть включен параметр 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 с интерактивными элементами.

Визуальное выделение привязанных элементов может улучшить UX, но привязанный элемент не отображается в дереве специальных возможностей. Визуальная тема, примененная к прикрепленному элементу, не должна указывать на то, что элемент активен или находится в фокусе, а кольцо фокуса клавиатуры должно быть хорошо видно и отличаться от выделения 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);
  });
}