Carousel title
"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>Gallery
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.
"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
ariaLabel on Carousel, Carousel.Thumbnails, and each Carousel.Thumbnail you render.previousAriaLabel and nextAriaLabel on Carousel.Controls to name the arrow buttons.