Skip to main content
Vuetify0 is now a release candidate!
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Carousel

Headless carousel built on CSS scroll-snap with multi-slide display and partial-slide peeking.

Usage

The Carousel provides slide navigation with native drag/swipe via CSS scroll-snap. Slides register with a step context for programmatic navigation.

Slide 1
<script setup lang="ts">
  import { Carousel } from '@vuetify/v0'
  import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
</script>

<template>
  <div class="flex flex-col gap-4">
    <Carousel.Root>
      <Carousel.Viewport class="rounded-lg overflow-hidden gap-4 cursor-grab data-[dragging]:cursor-grabbing">
        <Carousel.Item
          v-for="i in 5"
          :key="i"
          class="flex items-center justify-center h-48 rounded-lg text-lg font-medium bg-surface-variant text-on-surface-variant w-full shrink-0"
          :value="i"
        >
          Slide {{ i }}
        </Carousel.Item>
      </Carousel.Viewport>

      <div class="flex items-center justify-center gap-2 mt-3">
        <Carousel.Previous class="p-2 rounded-full border border-divider hover:bg-surface-variant disabled:opacity-40">
          <svg class="size-5" viewBox="0 0 24 24"><path :d="mdiChevronLeft" fill="currentColor" /></svg>
        </Carousel.Previous>

        <Carousel.Next class="p-2 rounded-full border border-divider hover:bg-surface-variant disabled:opacity-40">
          <svg class="size-5" viewBox="0 0 24 24"><path :d="mdiChevronRight" fill="currentColor" /></svg>
        </Carousel.Next>
      </div>
    </Carousel.Root>
  </div>
</template>

Anatomy

vue
<script setup lang="ts">
  import { Carousel } from '@vuetify/v0'
</script>

<template>
  <Carousel.Root>
    <Carousel.Viewport>
      <Carousel.Item />
    </Carousel.Viewport>

    <Carousel.Indicator />

    <Carousel.Previous />

    <Carousel.Next />

    <Carousel.Progress />

    <Carousel.LiveRegion />
  </Carousel.Root>
</template>

Examples

A photo gallery that folds every navigation affordance into one cohesive surface: dot indicators, overlay Previous/Next arrows, autoplay with a pause toggle and a timer bar, and a peek viewport that reveals the edges of the neighbouring slides. The selected slide drives a caption panel below the carousel, so the demo also shows how to read the active value back out.

The gallery leans on the full compound surface. Carousel.Root is circular with :autoplay set to a fixed interval, and its v-model carries the active slide value — the same value Carousel.Item registers with :value. Carousel.Indicator hands each dot an attrs object (role, aria-selected, aria-controls, and roving tabindex) that you bind with v-bind="item.attrs" for keyboard navigation at no cost. The peek is pure CSS: horizontal padding on Carousel.Viewport carves the bleed, and the component mirrors that padding onto scroll-padding so snap points stay aligned. Autoplay does not start on its own — the play button calls the root’s play and stop slot props, and the carousel auto-pauses the timer during drag and touch. Carousel.Progress visualizes the remaining time, faded out via data-[state=idle]:opacity-0 until playback begins.

Reach for this layout when a single carousel needs to be discoverable through several input paths at once — common for marketing heroes, onboarding flows, and product galleries. The tradeoff is surface area: a barebones swipe carousel needs only Root, Viewport, and Item. Because navigation is built on createStep, you get the same first/last/next/prev model the Step provider exposes; for fade transitions instead of scroll-snap sliding, compose Step with Presence rather than reaching for this scroll-based layout.

FileRole
useGallery.tsOwns the photo data, the autoplay interval, and the derived active photo plus position
Gallery.vueRenders the carousel compound — peek viewport, overlay arrows, autoplay toggle, progress bar, and indicator dots
gallery.vueWires the composable to the gallery and shows a caption panel reflecting the selected slide
Alpine LakeDolomites, Italy
Alpine LakeDolomites, Italy
1 / 5

Drag, or use the arrows and dots to browse. Press Play to auto-advance — autoplay pauses automatically while you drag.

Recipes

Circular Navigation

Enable wrapping from last slide to first with the circular prop.

vue
<template>
  <Carousel.Root circular>
    <Carousel.Viewport>
      <Carousel.Item v-for="i in 5" :key="i" :value="i">
        Slide {{ i }}
      </Carousel.Item>
    </Carousel.Viewport>
  </Carousel.Root>
</template>

Vertical Orientation

Set orientation="vertical" for a vertically scrolling carousel.

vue
<template>
  <Carousel.Root orientation="vertical">
    <Carousel.Viewport style="height: 300px">
      <Carousel.Item v-for="i in 5" :key="i" :value="i">
        Slide {{ i }}
      </Carousel.Item>
    </Carousel.Viewport>
  </Carousel.Root>
</template>

Disabled State

Disable all navigation via the disabled prop on the root.

vue
<template>
  <Carousel.Root disabled>
    <Carousel.Viewport>
      <Carousel.Item v-for="i in 3" :key="i" :value="i">
        Slide {{ i }}
      </Carousel.Item>
    </Carousel.Viewport>

    <Carousel.Previous>Previous</Carousel.Previous>
    <Carousel.Next>Next</Carousel.Next>
  </Carousel.Root>
</template>

Instant Scroll

Set behavior="instant" to disable smooth scrolling on programmatic navigation.

vue
<template>
  <Carousel.Root behavior="instant">
    <Carousel.Viewport>
      <Carousel.Item v-for="i in 5" :key="i" :value="i">
        Slide {{ i }}
      </Carousel.Item>
    </Carousel.Viewport>
  </Carousel.Root>
</template>

Styling with Data Attributes

All component state is exposed via data-* attributes for CSS-only styling:

css
/* Style the selected slide */
[data-selected] { opacity: 1; }

/* Dim inactive slides */
[aria-hidden="true"] { opacity: 0.4; }

/* Hide nav buttons at boundaries */
[data-edge] { visibility: hidden; }

/* Drag interaction */
[data-dragging] { cursor: grabbing; user-select: none; }

Accessibility

The Carousel implements the WAI-ARIA Carousel Pattern↗︎.

ElementRole / Attribute
Rootrole="region", aria-roledescription="carousel", aria-label, aria-disabled
Viewportaria-live="polite"
Sliderole="group", aria-roledescription="slide", aria-label="N of M"
Previousaria-label defaults to "Previous slide" (Carousel.prev key, localizable), aria-controls links to viewport
Nextaria-label defaults to "Next slide" (Carousel.next key, localizable), aria-controls links to viewport
Indicatorrole="tablist" container with aria-orientation, role="tab" per dot, aria-selected
Progressrole="progressbar", aria-valuenow, aria-valuemin, aria-valuemax
LiveRegionrole="status", aria-live="polite", aria-atomic

Slides outside the visible window are marked with aria-hidden="true" so screen readers only announce visible content. The LiveRegion provides a dedicated announcement channel for slide changes, using a 100ms delay for reliable screen reader detection.

FAQ

Discord
Need help? Join our community for support and discussions ↗
Was this page helpful?

Ctrl+/