Carousel
Horizontal or vertical scrollable slide region built on Embla. Touch, drag, keyboard, plugins.
Overview
Carousel is the sliding-content primitive: a region of slides that scrolls horizontally (default) or vertically, with drag, touch, keyboard arrows, and optional plugins (autoplay, wheel, fade). Built on Embla Carousel, it exposes a controlled API via setApi so you can read state (current slide, can-scroll-prev/next) and call methods (scrollTo, scrollNext) from outside.
Carousels hide content past the first slide - use only when order matters less than presence (galleries, testimonials, partner logos). For prioritized content, prefer a vertical list. Always pair arrow controls with keyboard navigation; any auto-rotation must be pausable.
Preview
'use client';import { Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from '@gremorie/rx-display';export function CarouselPreview() { return ( <Carousel className="w-full max-w-sm"> <CarouselContent> {Array.from({ length: 5 }).map((_, i) => ( <CarouselItem key={i}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-semibold">{i + 1}</span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> );}Anatomy
Carousel Embla root + context provider (orientation, opts, plugins)
├─ CarouselContent overflow viewport + flex track
│ └─ CarouselItem a single slide (basis-full by default)
├─ CarouselPrevious previous-slide arrow button, auto-disabled at the start
└─ CarouselNext next-slide arrow button, auto-disabled at the endInstallation
bash npx gremorie@latest add rx-carousel bash pnpm dlx gremorie@latest add rx-carousel bash yarn dlx gremorie@latest add rx-carousel bash bunx --bun gremorie@latest add rx-carousel Usage
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
} from "@gremorie/rx-display";
export function Example() {
return (
<Carousel className="w-full max-w-sm">
<CarouselContent>
{Array.from({ length: 5 }).map((_, i) => (
<CarouselItem key={i}>
<div className="p-1">Slide {i + 1}</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
);
}npx gremorie@latest add ng-carouselimport { Component } from '@angular/core';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
} from '@gremorie/ng-display';
@Component({
selector: 'app-example',
standalone: true,
imports: [
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
],
template: `
<gr-carousel class="w-full max-w-xs">
<gr-carousel-content>
@for (slide of slides; track slide) {
<gr-carousel-item>
<div class="p-1">Slide {{ slide }}</div>
</gr-carousel-item>
}
</gr-carousel-content>
<gr-carousel-previous />
<gr-carousel-next />
</gr-carousel>
`,
})
export class ExampleComponent {
readonly slides = [1, 2, 3, 4, 5];
}API
<Carousel>
Root container. Sets up Embla, exposes context for CarouselContent, CarouselItem, CarouselPrevious, and CarouselNext.
| Prop | Type | Default | Description |
|---|---|---|---|
opts | EmblaOptionsType | - | Embla options. Common keys: align: "start" | "center" | "end", loop: boolean, dragFree: boolean. See Embla docs. |
plugins | EmblaPluginType[] | - | Embla plugins. Common: Autoplay, WheelGestures, ClassNames, Fade. |
orientation | "horizontal" | "vertical" | "horizontal" | Sets the Embla axis and styles CarouselContent and arrow positions accordingly. |
setApi | (api: CarouselApi) => void | - | Receives the Embla API instance once initialized. Use to control the carousel from outside (call api.scrollTo(2), read api.selectedScrollSnap()). |
className | string | - | Extra classes on the root div. |
The root renders with role="region" and aria-roledescription="carousel" so AT announces it correctly.
<CarouselContent>
The viewport's scrolling track. Internal structure: an outer div.overflow-hidden (the viewport) plus an inner flex track. Uses -ml-4 (horizontal) or -mt-4 flex-col (vertical) to compensate for the per-item left/top padding.
<CarouselItem>
A single slide. Renders with role="group" and aria-roledescription="slide". By default each slide takes basis-full (one slide per view); change basis-1/2, basis-1/3, etc. on className to show multiple slides at once.
<CarouselPrevious> and <CarouselNext>
Pre-styled arrow buttons (built on Button from @gremorie/rx-forms with variant="outline" and size="icon"). Auto-disable when there is no previous/next slide. Position absolutely outside the carousel (-left-12 / -right-12).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | inherited from Button | "outline" | Visual style. |
size | inherited from Button | "icon" | Size token. |
className | string | - | Extra classes - useful when the default -left-12 position clips. |
All other Button props are forwarded.
CarouselApi
Type alias for the Embla API. Use with setApi:
const [api, setApi] = useState<CarouselApi | null>(null);
useEffect(() => {
if (!api) return;
api.on('select', () => {
console.log('Current slide:', api.selectedScrollSnap());
});
}, [api]);
<Carousel setApi={setApi}>...</Carousel>;Composition
<Carousel>owns Embla and provides context.<CarouselContent>is the scrolling track. Required wrapper around items.<CarouselItem>is each slide. Customizebasis-*for multi-slide views.<CarouselPrevious>/<CarouselNext>are optional but recommended for keyboard and mouse users.
The Carousel registers a keydown handler on the root that scrolls on ArrowLeft / ArrowRight regardless of where focus is inside.
Variations
Multi-slide view
'use client';import { Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from '@gremorie/rx-display';export function CarouselSizesPreview() { return ( <Carousel className="w-full max-w-sm" opts={{ align: 'start' }}> <CarouselContent className="-ml-2"> {Array.from({ length: 6 }).map((_, i) => ( <CarouselItem key={i} className="basis-1/3 pl-2"> <Card> <CardContent className="flex aspect-square items-center justify-center p-3"> <span className="text-xl font-semibold">{i + 1}</span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> );}Each CarouselItem defaults to basis-full (one slide per view). Set basis-1/2, basis-1/3, etc. to show multiple slides at once, and pair opts={{ align: 'start' }} so the track aligns cleanly.
Vertical carousel
'use client';import { Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from '@gremorie/rx-display';export function CarouselVerticalPreview() { return ( <Carousel orientation="vertical" opts={{ align: 'start' }} className="w-full max-w-xs" > <CarouselContent className="-mt-2 h-[300px]"> {Array.from({ length: 5 }).map((_, i) => ( <CarouselItem key={i} className="basis-1/3 pt-2"> <Card> <CardContent className="flex items-center justify-center p-6"> <span className="text-2xl font-semibold">{i + 1}</span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> );}When orientation="vertical", set an explicit height on the inner CarouselContent - Embla needs bounded dimensions on the scroll axis. The arrow controls reposition to the top and bottom automatically.
With Embla plugin (autoplay)
import Autoplay from 'embla-carousel-autoplay';
<Carousel
plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
opts={{ loop: true }}
>
<CarouselContent>...</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>;stopOnInteraction: true is non-negotiable - users who interact with the carousel should not have it keep moving under them.
Accessibility
- Roles: root is
role="region"witharia-roledescription="carousel"; each slide isrole="group"witharia-roledescription="slide". Add anaria-labelon the root identifying what the carousel shows ("Customer testimonials", "Product gallery"). - Keyboard:
ArrowLeftandArrowRightscroll between slides (handled by the rootkeydowncapture).Tabmoves focus through the arrow controls and any focusable content inside slides. - Arrow controls:
CarouselPreviousandCarouselNextship with visually hidden text ("Previous slide", "Next slide") so AT users know what each button does. They auto-disable when no more slides are available. - Auto-rotation: if you use
Autoplay, always passstopOnInteraction: trueand ensureprefers-reduced-motionusers get a static experience (skip the plugin entirely or setdelay: 0). - Live announcements: for galleries where the active slide changes meaning, wrap a sibling
aria-live="polite"region that announces the current slide's title.
Related
- Card - frequent slide content.
- Tabs - use when slides represent distinct contexts that users navigate to deliberately rather than browse.
- Embla Carousel docs - full plugin and options reference.