Carrousel met Snap-hoogtepunten

Met containerquery's voor de scrollstatus kun je elementen opmaken op basis van hun huidige scrollstatus, bijvoorbeeld of een element vastzit (via sticky positionering) of is uitgelijnd (via scrolluitlijning). Dit maakt carrousel- of galerijfunctionaliteit mogelijk waarbij het actieve item visueel kan worden onderscheiden zonder afhankelijk te zijn van JavaScript-intersection observers of scrollgebeurtenislisteners.

Kernimplementatie

Als je uitgelijnde items wilt markeren, moet je een scrolluitlijningscontainer maken, de uitlijningsdoelen definiëren als scrollstatuscontainers en die status opvragen om onderliggende elementen op te maken.

1. De container voor scrolluitlijning instellen

Voor de bovenliggende container moet scroll-snap-type aanstaan.

<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. Snapdoelen definiëren als containers voor de scrollstatus

Elk item in de carrousel dat moet worden bijgehouden voor vastmaken, moet worden gedefinieerd als een scroll-state-container.

.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. De status van snapped opvragen

Omdat containerquery's afstammelingen opmaken, moet je de markeringsstijlen toepassen op een element binnen het uitlijndoel. Omdat de scrollcontainer is ingesteld op overloop op de x-as, gebruik je de query scroll-state(snapped: x).

VERPLICHT: Verpak de stijlen in @media (prefers-reduced-motion: no-preference) om het effect alleen te tonen aan gebruikers die niet hebben gevraagd om minder beweging. Afhankelijk van je use case kun je delen van het effect behouden, maar in dit geval knipperen de kaarten van wit naar blauw op een manier die problemen kan veroorzaken voor sommige gebruikers. Daarom zetten we het effect helemaal uit.

/* 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;
  }
}

De descriptor snapped kan specifieke assen opvragen: x, y, inline, block of both.

Toegankelijkheid

AVOID: het gebruik van scroll-state met interactieve elementen.

Visuele highlights voor vastgezette items kunnen de gebruikerservaring verbeteren, maar het vastgezette item wordt niet blootgesteld aan de toegankelijkheidsstructuur. Het visuele thema dat op een vastgezet item wordt toegepast, mag niet aangeven dat het element actief is of de focus heeft. Een toetsenbordfocusring moet duidelijk zichtbaar zijn en zich onderscheiden van de snapped-markering. Als het vastgezette item interactief is, moet je andere standaard toegankelijkheidspraktijken gebruiken om het toegankelijk te maken.

Uitlijnen gebeurt door te scrollen, waardoor de toetsenbordfocus niet wordt verplaatst. Toetsenbordfocus kan er wel voor zorgen dat de scrollcontainer wordt verplaatst, waardoor het vastgezette item verandert. Dit kan het item met de focus zijn, maar dat hoeft niet. Dit kan verwarrend zijn voor gebruikers en wordt afgeraden.

Reservestrategieën

Voor browsers die geen scroll-state-query's ondersteunen, moet je een functionele basisfunctionaliteit bieden waarbij alle items leesbaar zijn, zelfs zonder de markering 'actief'.

Functiedetectie

Je kunt @supports gebruiken om alleen verbeteringen te bieden aan ondersteunde browsers:

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

JavaScript-reserve

Als de markering essentieel is voor de gebruikerservaring, gebruik je IntersectionObserver om het vastgezette item te bepalen. Pas het waargenomen gebied aan tot een dunne strook in het midden van de carrousel door een rootMargin met een negatieve inline waarde op te geven. Als u een element bijvoorbeeld als overlappend wilt beschouwen als het zich in de middelste 2% van de carrousel bevindt, stelt u de rootMargin in op "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);
  });
}