Skip to content

Carousel

A row (or column) of slides that scrolls and snaps, with previous and next buttons and slide dots, for a small set of related items such as featured cards or screenshots. Use CarouselItem for each slide. Built on native scroll snapping, so touch and trackpad swiping work without extra code.

Displayv0.1.0experimentalWCAG 2.2 AAView spec

Spring collection

Light layers in soft colors, made to last more than one season.

Trail guide

Twelve routes for a long weekend, with water stops marked.

Studio notes

How the team sketches, tests and ships a small idea in a week.

Quiet hours

A focus mode that mutes everything except the people you pick.

Field report

What we learned from watching forty people use the first version.

import { Carousel, CarouselItem } from "@rdloom/react";
function SlideCard({ art, title, text }: { art: string; title: string; text: string }) {
  return (
    <div className="overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
      <div aria-hidden="true" className={`h-40 ${art}`} />
      <div className="flex flex-col gap-1 p-4">
        <h3 className="text-sm font-semibold text-[var(--rd-color-text-default)]">{title}</h3>
        <p className="text-sm text-[var(--rd-color-text-muted)]">{text}</p>
      </div>
    </div>
  );
}

export default function CarouselBasicExample() {
  return (
    <div className="mx-auto w-[28rem] max-w-full">
      <Carousel label="Featured stories">
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-400),var(--rd-color-amber-400))]" title="Spring collection" text="Light layers in soft colors, made to last more than one season." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-blue-400),var(--rd-color-green-400))]" title="Trail guide" text="Twelve routes for a long weekend, with water stops marked." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-amber-400),var(--rd-color-ember-600))]" title="Studio notes" text="How the team sketches, tests and ships a small idea in a week." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-green-400),var(--rd-color-blue-600))]" title="Quiet hours" text="A focus mode that mutes everything except the people you pick." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-300),var(--rd-color-ember-700))]" title="Field report" text="What we learned from watching forty people use the first version." />
        </CarouselItem>
      </Carousel>
    </div>
  );
}

Installation

npx rdloom add carousel

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/carousel.json

Works, but without upgrade tracking.

Usage

import { Carousel, CarouselItem } from "@rdloom/react";

    <div className="overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
      <div aria-hidden="true" className={`h-40 ${art}`} />
      <div className="flex flex-col gap-1 p-4">
        <h3 className="text-sm font-semibold text-[var(--rd-color-text-default)]">{title}</h3>
        <p className="text-sm text-[var(--rd-color-text-muted)]">{text}</p>
      </div>
    </div>
  );
}

export default function CarouselBasicExample() {
  return (
    <div className="mx-auto w-[28rem] max-w-full">
      <Carousel label="Featured stories">
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-400),var(--rd-color-amber-400))]" title="Spring collection" text="Light layers in soft colors, made to last more than one season." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-blue-400),var(--rd-color-green-400))]" title="Trail guide" text="Twelve routes for a long weekend, with water stops marked." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-amber-400),var(--rd-color-ember-600))]" title="Studio notes" text="How the team sketches, tests and ships a small idea in a week." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-green-400),var(--rd-color-blue-600))]" title="Quiet hours" text="A focus mode that mutes everything except the people you pick." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-300),var(--rd-color-ember-700))]" title="Field report" text="What we learned from watching forty people use the first version." />
        </CarouselItem>
      </Carousel>
    </div>

Multiple per view

Spring collection

Light layers in soft colors, made to last more than one season.

Trail guide

Twelve routes for a long weekend, with water stops marked.

Studio notes

How the team sketches, tests and ships a small idea in a week.

Quiet hours

A focus mode that mutes everything except the people you pick.

Field report

What we learned from watching forty people use the first version.

import { Carousel, CarouselItem } from "@rdloom/react";
function SlideCard({ art, title, text }: { art: string; title: string; text: string }) {
  return (
    <div className="overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
      <div aria-hidden="true" className={`h-40 ${art}`} />
      <div className="flex flex-col gap-1 p-4">
        <h3 className="text-sm font-semibold text-[var(--rd-color-text-default)]">{title}</h3>
        <p className="text-sm text-[var(--rd-color-text-muted)]">{text}</p>
      </div>
    </div>
  );
}

export default function CarouselMultiplePerViewExample() {
  return (
    <div className="mx-auto w-[44rem] max-w-full">
      <Carousel label="More stories" slidesPerView={2} gap="md" loop>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-400),var(--rd-color-amber-400))]" title="Spring collection" text="Light layers in soft colors, made to last more than one season." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-blue-400),var(--rd-color-green-400))]" title="Trail guide" text="Twelve routes for a long weekend, with water stops marked." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-amber-400),var(--rd-color-ember-600))]" title="Studio notes" text="How the team sketches, tests and ships a small idea in a week." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-green-400),var(--rd-color-blue-600))]" title="Quiet hours" text="A focus mode that mutes everything except the people you pick." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-300),var(--rd-color-ember-700))]" title="Field report" text="What we learned from watching forty people use the first version." />
        </CarouselItem>
      </Carousel>
    </div>
  );
}

With autoplay

Spring collection

Light layers in soft colors, made to last more than one season.

Trail guide

Twelve routes for a long weekend, with water stops marked.

Studio notes

How the team sketches, tests and ships a small idea in a week.

Quiet hours

A focus mode that mutes everything except the people you pick.

import { Carousel, CarouselItem } from "@rdloom/react";
function SlideCard({ art, title, text }: { art: string; title: string; text: string }) {
  return (
    <div className="overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
      <div aria-hidden="true" className={`h-40 ${art}`} />
      <div className="flex flex-col gap-1 p-4">
        <h3 className="text-sm font-semibold text-[var(--rd-color-text-default)]">{title}</h3>
        <p className="text-sm text-[var(--rd-color-text-muted)]">{text}</p>
      </div>
    </div>
  );
}

export default function CarouselWithAutoplayExample() {
  return (
    <div className="mx-auto w-[28rem] max-w-full">
      <Carousel label="Highlights" autoplay autoplayInterval={4000}>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-400),var(--rd-color-amber-400))]" title="Spring collection" text="Light layers in soft colors, made to last more than one season." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-blue-400),var(--rd-color-green-400))]" title="Trail guide" text="Twelve routes for a long weekend, with water stops marked." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-amber-400),var(--rd-color-ember-600))]" title="Studio notes" text="How the team sketches, tests and ships a small idea in a week." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-green-400),var(--rd-color-blue-600))]" title="Quiet hours" text="A focus mode that mutes everything except the people you pick." />
        </CarouselItem>
      </Carousel>
    </div>
  );
}

Controlled

Spring collection

Light layers in soft colors, made to last more than one season.

Trail guide

Twelve routes for a long weekend, with water stops marked.

Studio notes

How the team sketches, tests and ships a small idea in a week.

Quiet hours

A focus mode that mutes everything except the people you pick.

Field report

What we learned from watching forty people use the first version.

Showing slide 1 of 5
import { useState } from "react";
import { Button, Carousel, CarouselItem } from "@rdloom/react";
function SlideCard({ art, title, text }: { art: string; title: string; text: string }) {
  return (
    <div className="overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
      <div aria-hidden="true" className={`h-40 ${art}`} />
      <div className="flex flex-col gap-1 p-4">
        <h3 className="text-sm font-semibold text-[var(--rd-color-text-default)]">{title}</h3>
        <p className="text-sm text-[var(--rd-color-text-muted)]">{text}</p>
      </div>
    </div>
  );
}

export default function CarouselControlledExample() {
  const [index, setIndex] = useState(0);
  return (
    <div className="mx-auto flex w-[28rem] max-w-full flex-col items-center gap-3">
      <Carousel label="Story picker" index={index} onIndexChange={setIndex} showDots={false}>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-400),var(--rd-color-amber-400))]" title="Spring collection" text="Light layers in soft colors, made to last more than one season." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-blue-400),var(--rd-color-green-400))]" title="Trail guide" text="Twelve routes for a long weekend, with water stops marked." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-amber-400),var(--rd-color-ember-600))]" title="Studio notes" text="How the team sketches, tests and ships a small idea in a week." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-green-400),var(--rd-color-blue-600))]" title="Quiet hours" text="A focus mode that mutes everything except the people you pick." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-300),var(--rd-color-ember-700))]" title="Field report" text="What we learned from watching forty people use the first version." />
        </CarouselItem>
      </Carousel>
      <div className="flex items-center gap-3">
        <Button variant="secondary" size="sm" onPress={() => setIndex(0)}>
          First
        </Button>
        <span className="text-sm text-[var(--rd-color-text-muted)]">Showing slide {index + 1} of 5</span>
        <Button variant="secondary" size="sm" onPress={() => setIndex(4)}>
          Last
        </Button>
      </div>
    </div>
  );
}

Vertical

Spring collection

Light layers in soft colors, made to last more than one season.

Trail guide

Twelve routes for a long weekend, with water stops marked.

Studio notes

How the team sketches, tests and ships a small idea in a week.

Quiet hours

A focus mode that mutes everything except the people you pick.

import { Carousel, CarouselItem } from "@rdloom/react";
function SlideCard({ art, title, text }: { art: string; title: string; text: string }) {
  return (
    <div className="overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
      <div aria-hidden="true" className={`h-40 ${art}`} />
      <div className="flex flex-col gap-1 p-4">
        <h3 className="text-sm font-semibold text-[var(--rd-color-text-default)]">{title}</h3>
        <p className="text-sm text-[var(--rd-color-text-muted)]">{text}</p>
      </div>
    </div>
  );
}

export default function CarouselVerticalExample() {
  return (
    <div className="mx-auto w-[22rem] max-w-full">
      <Carousel label="Release notes" orientation="vertical">
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-ember-400),var(--rd-color-amber-400))]" title="Spring collection" text="Light layers in soft colors, made to last more than one season." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-blue-400),var(--rd-color-green-400))]" title="Trail guide" text="Twelve routes for a long weekend, with water stops marked." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-amber-400),var(--rd-color-ember-600))]" title="Studio notes" text="How the team sketches, tests and ships a small idea in a week." />
        </CarouselItem>
        <CarouselItem>
          <SlideCard art="bg-[linear-gradient(135deg,var(--rd-color-green-400),var(--rd-color-blue-600))]" title="Quiet hours" text="A focus mode that mutes everything except the people you pick." />
        </CarouselItem>
      </Carousel>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
labelrequired

What the carousel shows, e.g. 'Featured plans'. It is the accessible name of the region.

stringnone
childrenrequired

CarouselItem elements, one per slide.

nodenone
orientation

Direction slides move in. A vertical carousel needs a height (classNames.viewport, default 18rem).

"horizontal" | "vertical""horizontal"
slidesPerView

How many slides are visible at once. Narrow containers show fewer: under 400px one, under 640px at most two.

1 | 2 | 31
loop

Next on the last slide goes back to the first, and Previous on the first goes to the last. Otherwise the buttons stop at the ends.

booleanfalse
showArrows

Show the Previous and Next buttons.

booleantrue
showDots

Show one 'Go to slide N' button per position.

booleantrue
autoplay

Move on by itself. A pause and play button is always shown with it. It stops while the pointer is over the carousel or focus is inside it, and never runs when the visitor prefers reduced motion. At the end it returns to the first slide.

booleanfalse
autoplayInterval

Milliseconds each slide stays when autoplay is on.

number5000
index

Controlled index of the first visible slide (0 is the first slide).

numbernone
defaultIndex

Index of the first visible slide at first, when uncontrolled.

number0
onIndexChange

Called when the first visible slide changes, whether by a button, a key, a dot, a swipe or autoplay.

(index: number) => voidnone
gap

Space between slides: 8, 16 or 24 px.

"sm" | "md" | "lg""md"
messages

Your own wording, for translation. goTo, slide and slides use {n}, {from}, {to} and {total} where they apply, e.g. 'Go to slide {n}'.

Partial<Record<"previous" | "next" | "goTo" | "dots" | "pause" | "play" | "slide" | "slides", string>>none
classNames

Extra class names for single parts. Keys: root, viewport, controls, arrow, dots, dot, autoplayButton.

Partial<Record<"root" | "viewport" | "controls" | "arrow" | "dots" | "dot" | "autoplayButton", string>>none

Accessibility

Role region, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab moves to the carousel, then through its buttons and the links in the visible slides
  • Left and Right arrows (Up and Down when vertical) move one slide while the carousel itself has focus
  • Home and End go to the first and last slide
  • Enter or Space on Previous, Next, a dot or the pause button does what its name says

Screen readers announce

  • The region is announced as 'Featured plans, carousel'
  • Each slide is announced as 'slide, 2 of 5'
  • After Next, 'Slide 3 of 5' is announced once; while autoplay runs nothing is announced

What your code must do

  • The region has role=region, aria-roledescription=carousel and an accessible name
  • Each slide has role=group, aria-roledescription=slide and a name like '2 of 5'
  • Slides that are fully out of view are inert, so Tab and screen readers skip them
  • Previous and Next are named buttons; at an end they use aria-disabled so focus is not lost
  • A change the person made is announced politely as 'Slide 2 of 5'; a change made by autoplay is not
  • Autoplay is off by default; when on it has a pause and play button, stops on hover and focus, and does not run under prefers-reduced-motion
  • Scrolling between slides is instant under prefers-reduced-motion
  • Dots are buttons of at least 24 px with aria-current on the current one

Guidelines

Use it when

  • A handful of equal items that do not all fit: featured products, screenshots, testimonials
  • Touch-first layouts where swiping is natural

Avoid it when

  • Content everyone must see: lay it out in a grid
  • Important announcements: rotating content is easy to miss
  • Long lists: use a list or table with paging

Don't

  • Turning on autoplay for content people need to read
  • Putting the only way to complete a task in a slide that is not shown at first
  • Slides with very different heights

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-default
  • --rd-color-border-default
  • --rd-color-action-primary
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-focus-ring
  • --rd-radius-control