Skip to main content

Carousel

Carousel is a content browser for exploring a horizontal collection, like browsing product photos or reviewing featured projects. It supports swiping, arrows, thumbnails, and automatic playback.

Import

import { Carousel } from '@vortexlabs/vortex';
import type { CarouselProps } from '@vortexlabs/vortex';

Other links

"use client";

import { Carousel, Stack, Text } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselBasicExample = () => {
  return (
    <Stack width="100%" maxWidth="1024px">
      <Carousel ariaLabel="Content rail">
        <Stack gap={6}>
          <Stack direction="row" align="center" justify="space-between" gap={4}>
            <Stack gap={1}>
              <Text as="h2" size="body-xl-desktop" weight="medium">
                Carousel title
              </Text>
              <Text size="body-sm-desktop" color="subtle">
                Description goes here.
              </Text>
            </Stack>
            <Carousel.Controls
              previousAriaLabel="Previous slides"
              nextAriaLabel="Next slides"
              size="lg"
            />
          </Stack>
          <Carousel.Viewport>
            {slides.map((value) => (
              <Carousel.Item key={value} value={value} width="300px">
                <Stack
                  height="260px"
                  backgroundColor="surface"
                  borderColor="default"
                  borderRadius="md"
                />
              </Carousel.Item>
            ))}
          </Carousel.Viewport>
        </Stack>
      </Carousel>
    </Stack>
  );
};

Features

  • Supports responsive slide widths and navigation groups.
  • Offers swipe navigation, arrows, indicators, and thumbnails.
  • Supports controlled selection, autoplay, and lazy mounting.
  • Supports RTL layouts and reduced-motion preferences.

Anatomy

Carousel contains one Carousel.Viewport, with a Carousel.Item for each slide. Carousel.Controls, Carousel.Indicator, and Carousel.Thumbnails are optional; each Carousel.Thumbnail selects a slide.

<Carousel ariaLabel="Project photos">
  <Carousel.Viewport>
    <Carousel.Item value="front">Front</Carousel.Item>
    <Carousel.Item value="side">Side</Carousel.Item>
    <Carousel.Indicator />
  </Carousel.Viewport>
  <Carousel.Controls previousAriaLabel="Previous photo" nextAriaLabel="Next photo" />
  <Carousel.Thumbnails ariaLabel="Choose a photo">
    <Carousel.Thumbnail value="front" ariaLabel="Show front">Front</Carousel.Thumbnail>
    <Carousel.Thumbnail value="side" ariaLabel="Show side">Side</Carousel.Thumbnail>
  </Carousel.Thumbnails>
</Carousel>

Set itemsPerView={1} to show one slide at a time. Give each Carousel.Item a unique value and use a matching defaultValue to choose the starting slide; otherwise, the first slide is selected.

Carousel.Indicator shows the current position with non-interactive dots. Add Carousel.Controls or Carousel.Thumbnails when users need buttons to select slides.

"use client";

import { Carousel, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselGalleryExample = () => {
  return (
    <Stack width="100%" maxWidth="480px">
      <Carousel ariaLabel="Gallery" itemsPerView={1} defaultValue="3">
        <Carousel.Viewport borderRadius="md">
          {slides.slice(0, 5).map((value) => (
            <Carousel.Item key={value} value={value}>
              <Stack
                height="320px"
                backgroundColor="surface"
                borderColor="default"
                borderRadius="md"
              />
            </Carousel.Item>
          ))}
          <Carousel.Indicator />
        </Carousel.Viewport>
      </Carousel>
    </Stack>
  );
};

Responsive Items

itemsPerView sets how many equally sized slides fit in the viewport. itemsPerScroll sets how many slides each navigation step advances; its default, "auto", advances by what fits in the viewport. Both accept responsive values.

With a numeric itemsPerScroll, slides form navigation groups and onChange reports the first slide's value in the selected group. For individual slide widths, omit itemsPerView and set width on each Carousel.Item.

"use client";

import { Carousel, Placeholder, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselResponsiveExample = () => {
  return (
    <Carousel
      ariaLabel="Responsive items"
      itemsPerView={{ xs: 1, sm: 2, md: 3 }}
      itemsPerScroll={{ xs: 1, md: 3 }}
    >
      <Stack gap={3}>
        <Stack direction="row" justify="end">
          <Carousel.Controls previousAriaLabel="Previous slides" nextAriaLabel="Next slides" />
        </Stack>
        <Carousel.Viewport gap={{ xs: 2, md: 4 }} borderRadius={{ xs: "sm", md: "lg" }}>
          {slides.map((value) => (
            <Carousel.Item key={value} value={value}>
              <Placeholder height="180px" minWidth={0} borderRadius={{ xs: "sm", md: "lg" }}>
                Slide {value}
              </Placeholder>
            </Carousel.Item>
          ))}
        </Carousel.Viewport>
      </Stack>
    </Carousel>
  );
};

Controls

Place Carousel.Controls inside Carousel to position arrows in your layout. Set position="overlay" to place them over Carousel.Viewport, as shown below.

With arrows="hover", the arrows appear on hover or keyboard focus and remain visible on touch devices.

"use client";

import { Carousel, Placeholder, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselOverlayExample = () => {
  return (
    <Carousel ariaLabel="Overlay controls" itemsPerView={3}>
      <Stack gap={3}>
        <Carousel.Viewport>
          {slides.map((value) => (
            <Carousel.Item key={value} value={value}>
              <Placeholder height="180px" minWidth={0}>
                Slide {value}
              </Placeholder>
            </Carousel.Item>
          ))}
          <Carousel.Controls
            previousAriaLabel="Previous slides"
            nextAriaLabel="Next slides"
            position="overlay"
            arrows="hover"
            rounded
          />
        </Carousel.Viewport>
      </Stack>
    </Carousel>
  );
};

Thumbnails

Place Carousel.Thumbnails inside Carousel, outside Carousel.Viewport, and match each thumbnail's value to a slide. Each thumbnail is a button, so use images or text inside it without nested links or buttons.

With a numeric itemsPerScroll, use one thumbnail per group and give it the first slide's value.

"use client";

import { Carousel, Placeholder, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselThumbnailsExample = () => {
  return (
    <Carousel ariaLabel="Thumbnail carousel" itemsPerView={1} itemsPerScroll={1}>
      <Stack gap={3}>
        <Stack direction="row" justify="end">
          <Carousel.Controls previousAriaLabel="Previous slides" nextAriaLabel="Next slides" />
        </Stack>
        <Carousel.Viewport>
          {slides.map((value) => (
            <Carousel.Item key={value} value={value}>
              <Placeholder height="180px" minWidth={0}>
                Slide {value}
              </Placeholder>
            </Carousel.Item>
          ))}
        </Carousel.Viewport>
        <Carousel.Thumbnails ariaLabel="Choose a slide" gap={{ xs: 2, md: 3 }} scrollbar="hover">
          {slides.map((value) => (
            <Carousel.Thumbnail
              key={value}
              value={value}
              ariaLabel={`Show slide ${value}`}
              width={{ xs: "64px", md: "96px" }}
              padding="2xs"
              borderRadius="sm"
            >
              <Placeholder height="48px" minWidth={0}>
                {value}
              </Placeholder>
            </Carousel.Thumbnail>
          ))}
        </Carousel.Thumbnails>
      </Stack>
    </Carousel>
  );
};

Automatic Playback

autoPlay adds a play/pause button and advances until the last slide or group. Set autoPlayInterval in milliseconds; the example advances every three seconds.

Hover, an offscreen carousel, or a hidden browser tab pauses playback temporarily. Focus or manual navigation stops it until the user presses play again; reduced-motion preferences also require an explicit start.

"use client";

import { Carousel, Placeholder, Stack } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselPlaybackExample = () => {
  return (
    <Carousel
      ariaLabel="Autoplay"
      itemsPerView={1}
      itemsPerScroll={1}
      autoPlay
      autoPlayInterval={3000}
      playLabel="Play slides"
      pauseLabel="Pause slides"
    >
      <Stack gap={3}>
        <Carousel.Viewport>
          {slides.map((value) => (
            <Carousel.Item key={value} value={value}>
              <Placeholder height="180px" minWidth={0}>
                Slide {value}
              </Placeholder>
            </Carousel.Item>
          ))}
          <Carousel.Indicator />
        </Carousel.Viewport>
      </Stack>
    </Carousel>
  );
};

Lazy Mounting

lazyMount mounts nearby slide content as users navigate and keeps it mounted afterward, preserving state such as the notes below.

Set itemsPerView or an explicit Carousel.Item width to reserve space; without either, the content mounts immediately. Use placeholder for content that has not mounted yet, with a height matching the slide content to avoid layout shifts.

"use client";

import { Carousel, Placeholder, Stack, TextField } from "@vortexlabs/vortex";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselLazyExample = () => {
  return (
    <Carousel ariaLabel="Lazy mounting" itemsPerView={1} itemsPerScroll={1} lazyMount>
      <Stack gap={3}>
        <Stack direction="row" justify="end">
          <Carousel.Controls previousAriaLabel="Previous slides" nextAriaLabel="Next slides" />
        </Stack>
        <Carousel.Viewport>
          {slides.map((value) => (
            <Carousel.Item
              key={value}
              value={value}
              placeholder={
                <Placeholder height="180px" minWidth={0}>
                  Not mounted yet
                </Placeholder>
              }
            >
              <Placeholder height="180px" minWidth={0}>
                <TextField.Root>
                  <TextField.Label>Notes for slide {value}</TextField.Label>
                  <TextField.Input
                    name={`notes-${value}`}
                    placeholder="Type a note, then navigate away and back"
                  />
                </TextField.Root>
              </Placeholder>
            </Carousel.Item>
          ))}
        </Carousel.Viewport>
      </Stack>
    </Carousel>
  );
};

Controlled Selection

Pass a slide's value and update it from onChange to control selection. Use defaultValue instead when Carousel should manage selection; it only sets the starting slide.

The buttons below update the same state to select a slide from outside the carousel.

Selected slide: 1
"use client";

import { Button, Carousel, Placeholder, Stack, Text } from "@vortexlabs/vortex";
import { useState } from "react";

const slides = ["1", "2", "3", "4", "5", "6"];

export const CarouselControlledExample = () => {
  const [value, setValue] = useState("1");

  return (
    <Stack gap={3}>
      <Stack direction="row" gap={2} wrap>
        {slides.map((slide) => (
          <Button
            key={slide}
            size="sm"
            variant={value === slide ? "primary" : "secondary"}
            onClick={() => setValue(slide)}
            attributes={{ "aria-pressed": value === slide }}
          >
            Slide {slide}
          </Button>
        ))}
      </Stack>
      <Carousel
        ariaLabel="Controlled carousel"
        itemsPerView={1}
        itemsPerScroll={1}
        value={value}
        onChange={({ value }) => setValue(value)}
      >
        <Stack gap={3}>
          <Stack direction="row" justify="end">
            <Carousel.Controls
              previousAriaLabel="Previous slides"
              nextAriaLabel="Next slides"
            />
          </Stack>
          <Carousel.Viewport>
            {slides.map((value) => (
              <Carousel.Item key={value} value={value}>
                <Placeholder height="180px" minWidth={0}>
                  Slide {value}
                </Placeholder>
              </Carousel.Item>
            ))}
          </Carousel.Viewport>
        </Stack>
      </Carousel>
      <Text>Selected slide: {value}</Text>
    </Stack>
  );
};

Accessibility

Key
Description
TabShiftTab
Move forward or backward through focusable elements.
EnterSpace
Activate a focused arrow, thumbnail, or playback button.
Left arrowRight arrow
Move focus between thumbnails when three or more are available.
HomeEnd
Focus the first or last thumbnail when three or more are available.
Provide ariaLabel on Carousel, Carousel.Thumbnails, and each Carousel.Thumbnail you render.
Provide previousAriaLabel and nextAriaLabel on Carousel.Controls to name the arrow buttons.