Skip to content
On this page Accessibility

Display

Carousel

Scroll-snap slides with previous and next, dots and an opt-in autoplay that always has a Pause button.

<script>
  import { Carousel, CarouselItem } from "@codeatlasdev/ui";
  let index = $state(0);
</script>

<Carousel label="Product highlights" bind:index>
  <CarouselItem>…</CarouselItem>
  <CarouselItem>…</CarouselItem>
</Carousel>

The replacement for shadcn Carousel (Embla). The strip is native scroll-snap, so touch, trackpad and the browser’s own scrolling work with no pointer code; the buttons and dots only move it, and a scroll by touch updates the dots. No JavaScript carousel engine, nothing to install.

#964

Highlights

Arrows, dots and a bound index.

Slide 1 of 4. Swipe, use the buttons, or focus the carousel and press the arrow keys.

Reference #964 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Carousel, CarouselItem } from '@codeatlasdev/ui';

  const slides = ['Plan', 'Build', 'Ship', 'Learn'];
  let index = $state(0);
</script>

<div class="frame">
  <Carousel label="Product highlights" bind:index>
    {#each slides as title (title)}
      <CarouselItem><div class="slide">{title}</div></CarouselItem>
    {/each}
  </Carousel>
</div>
<p class="note">Slide {index + 1} of {slides.length}. Swipe, use the buttons, or focus the carousel and press the arrow keys.</p>

<style>
  .frame { max-width: 28rem; }
  .slide {
    display: grid;
    place-items: center;
    height: 9rem;
    margin-inline: var(--ca-space-1);
    border-radius: var(--ca-radius-md);
    background: var(--ca-surface-sunken);
    color: var(--ca-text-primary);
    font-size: var(--ca-font-size-h4);
  }
  .note { margin: var(--ca-space-2) 0 0; color: var(--ca-text-secondary); font-size: var(--ca-font-size-caption); }
</style>
#965

Several per view

`perView` shows three cards; `loop` wraps at the ends.

Reference #965 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Carousel, CarouselItem } from '@codeatlasdev/ui';

  const cards = ['Starter', 'Team', 'Business', 'Enterprise', 'Partner'];
</script>

<div class="frame">
  <Carousel label="Plans" perView={3} loop>
    {#each cards as name (name)}
      <CarouselItem><div class="card">{name}</div></CarouselItem>
    {/each}
  </Carousel>
</div>

<style>
  .frame { max-width: 36rem; }
  .card {
    display: grid;
    place-items: center;
    height: 6rem;
    margin-inline: var(--ca-space-1);
    border: var(--ca-border-width) solid var(--ca-outline-default);
    border-radius: var(--ca-radius-md);
    color: var(--ca-text-primary);
  }
</style>
#966

Autoplay

Every 4 seconds, with Pause.

Reference #966 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Carousel, CarouselItem } from '@codeatlasdev/ui';

  const quotes = ['"Setup took ten minutes."', '"The keyboard support is real."', '"Our audit passed first time."'];
</script>

<div class="frame">
  <Carousel label="Customer quotes" autoplay={4000} loop arrows={false}>
    {#each quotes as quote (quote)}
      <CarouselItem><blockquote class="quote">{quote}</blockquote></CarouselItem>
    {/each}
  </Carousel>
</div>

<style>
  .frame { max-width: 28rem; }
  .quote {
    display: grid;
    place-items: center;
    height: 7rem;
    margin: 0;
    padding-inline: var(--ca-space-4);
    color: var(--ca-text-primary);
    font-size: var(--ca-font-size-h5);
    text-align: center;
  }
</style>

Accessibility

It follows the WAI-ARIA carousel pattern. The carousel is a labelled role="region" with aria-roledescription="carousel"; each slide is a role="group" named “2 of 4”. Slides outside the view are inert, so their links and buttons leave the tab order and the reading order. The dots are labelled buttons with aria-current on the current one (the slides are not tab panels, so they are not a tablist). The slide strip is the one tab stop (it scrolls, so it must be reachable without a pointer, even when no slide holds a control). With it focused, Left and Right (mirrored in RTL), Home and End move the slides.

Autoplay behavior

Off unless you set autoplay (milliseconds). When on, a Pause / Start button appears, rotation stops while focus or the pointer is inside, and the strip is aria-live="off" while it rotates (a rotating live region would read every slide) and polite once it stops. A manual move stops it for good. Under prefers-reduced-motion: reduce it never runs, the button is not shown and scrolling is instant. Without loop it stops on the last slide.

Props

PropTypeDefaultDescription
labelstring–Accessible name of the carousel. Required.
indexnumber0First visible slide, 0-based. Bindable.
perViewnumber1Slides visible at once.
loopbooleanfalseWrap around at the ends.
dotsbooleantrueShow the dots.
arrowsbooleantrueShow previous and next.
autoplaynumberundefinedMilliseconds between slides; unset means no autoplay.
onchange(index: number) => voidundefinedCalled when the current slide changes.
classstring''Extra class names.

CarouselItem takes only class; give it one child that carries the slide’s size and look.