Skip to content

Carousel

<div class="carousel">
<div class="carousel-content" aria-label="Featured">
<div class="carousel-item"></div>
<div class="carousel-item"></div>
</div>
<button class="btn btn-outline btn-icon carousel-previous" type="button"></button>
<button class="btn btn-outline btn-icon carousel-next" type="button"></button>
</div>
ClassPart
.carouselThe wrapper the buttons are positioned against
.carousel-contentThe scroller
.carousel-itemOne slide
.carousel-previous / .carousel-nextThe buttons
AttributeOnMeaning
data-orientation="vertical".carouselScrolls down instead of across
data-index.carouselWhich slide is showing. Set by the runtime

A slide is basis-full by default. Change the basis and several fit at once.

document.addEventListener("pk:carousel:change", (event) => {
console.log(event.detail.index, event.detail.atEnd);
});

The movement is scroll-snap-type — the browser owns it. That means swipe on a phone, momentum on a trackpad, the scrollbar if the user wants it, and scroll-behavior: smooth that goes instant under reduced motion. The runtime only drives the buttons and reports which slide is showing; remove the script and the carousel still scrolls.

Set dir="rtl" on the page or on any subtree — the buttons swap ends and the arrow keys follow the reading direction. See the button page for a live example.

  • The region announces itself as a carousel, and each slide as a slide with its position — “2 of 5” — so the count is known without seeing it.
  • The scroller is focusable and takes the arrow keys, so it can be moved through without a pointer and without reaching for the buttons.
  • Name the scroller. aria-label on .carousel-content says what the set is; “carousel” alone says nothing.
  • The buttons disable at the ends rather than wrapping, so a keyboard user can tell where the set finishes.
  • Off-screen slides are still in the DOM and still tabbable. If a slide holds links, that is what you want; if it holds a form, consider whether a carousel is the right shape at all.