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.
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 carouselCopies 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.jsonWorks, 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
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
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
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
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.
| Prop | Type | Default |
|---|---|---|
labelrequiredWhat the carousel shows, e.g. 'Featured plans'. It is the accessible name of the region. | string | none |
childrenrequiredCarouselItem elements, one per slide. | node | none |
orientationDirection slides move in. A vertical carousel needs a height (classNames.viewport, default 18rem). | "horizontal" | "vertical" | "horizontal" |
slidesPerViewHow many slides are visible at once. Narrow containers show fewer: under 400px one, under 640px at most two. | 1 | 2 | 3 | 1 |
loopNext 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. | boolean | false |
showArrowsShow the Previous and Next buttons. | boolean | true |
showDotsShow one 'Go to slide N' button per position. | boolean | true |
autoplayMove 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. | boolean | false |
autoplayIntervalMilliseconds each slide stays when autoplay is on. | number | 5000 |
indexControlled index of the first visible slide (0 is the first slide). | number | none |
defaultIndexIndex of the first visible slide at first, when uncontrolled. | number | 0 |
onIndexChangeCalled when the first visible slide changes, whether by a button, a key, a dot, a swipe or autoplay. | (index: number) => void | none |
gapSpace between slides: 8, 16 or 24 px. | "sm" | "md" | "lg" | "md" |
messagesYour 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 |
classNamesExtra 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