tetra-ui Logotetra ui

Carousel

Swipe between pages or an inline rail of items, with optional looping, autoplay, and composed controls.

carousel

Installation

npx shadcn@latest add @tetra-ui/carousel

Setup

Wrap your app in GestureHandlerRootView (or use the tetra-ui ThemeProvider, which already includes it):

import { GestureHandlerRootView } from "react-native-gesture-handler";

export default function RootLayout({ children }) {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>{children}</GestureHandlerRootView>
  );
}

Set an explicit height on Carousel. The track fills the space above the controls.

Usage

import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";
import { Stack } from "@/components/ui/stack";
<Carousel className="h-64">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <Stack className="items-center justify-between" direction="row">
    <CarouselPrevious />
    <CarouselDots />
    <CarouselNext />
  </Stack>
</Carousel>

Composition

Carousel
├── CarouselContent
│   └── CarouselItem
├── CarouselPrevious
├── CarouselDots
└── CarouselNext

Every piece except the root, content, and items is optional. Controls are in-flow under the track — place Previous, Dots, and Next in a row.

Variants

page (default) makes each item the full track width. inline sizes items evenly so more than one is visible. perView is how many items fit in the viewport. A fractional value such as 1.2 leaves leftover space that is split so you see a peek of the previous card and the next. gap is the pixel space between items and is used for snap math.

Padding the carousel with px-4 (or similar) clips neighbors at that edge. Use inset instead when you want the settled item padded but previous and next items to enter from the track edge:

<Carousel className="h-80 w-full" inset={16}>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <Stack className="px-4" direction="row">
    <CarouselPrevious />
    <CarouselDots />
    <CarouselNext />
  </Stack>
</Carousel>
<Carousel className="h-48" perView={2} variant="inline">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>

Loop

Pass loop to wrap from the last item back to the first. Looping is ignored when there are not enough items (page with fewer than 2, or inline with count <= perView).

<Carousel className="h-64" loop>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>

Autoplay

autoplay advances one item at a time. It pauses while dragging, when the app backgrounds, and when Reduce Motion is enabled. Without loop, it stops on the last item.

<Carousel autoplay autoplayInterval={4000} className="h-64" loop>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <CarouselDots />
</Carousel>

Controlled index

const [index, setIndex] = useState(0);

<Carousel className="h-64" index={index} onIndexChange={setIndex}>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>

useCarousel() and a CarouselRef (scrollTo, scrollNext, scrollPrev, getIndex) expose the same navigation.

API

Prop

Type

CarouselContent

Overflow-hidden viewport for the swipeable track.

CarouselItem

A single snap target. Width comes from the parent variant / perView.

CarouselPrevious / CarouselNext

Icon buttons built on Button. Disabled at the ends when loop is false.

Prop

Type

CarouselDots

Renders one pressable, progress-linked indicator per item. Omit this on long inline rails.

useCarousel

Must be called inside Carousel. Returns { index, count, looping, canScrollPrev, canScrollNext, scrollTo, scrollNext, scrollPrev, progress }.

On this page