Carousel
A row of slides that the user moves through with buttons, swipes or arrow keys.
@zenoxai/ui/components/carousel Markdownfunction CarouselDemo() {
const threats = [
{ name: "Phishing kits", count: 18 },
{ name: "Lookalike domains", count: 42 },
{ name: "Leaked credentials", count: 7 },
{ name: "Exposed services", count: 23 },
{ name: "Fake social profiles", count: 5 },
]
return (
<Carousel aria-label="Threats this week" className="w-full max-w-xs">
<CarouselContent>
{threats.map((threat, index) => (
<CarouselItem
key={threat.name}
aria-label={`${index + 1} of ${threats.length}`}
>
<Card>
<CardContent className="flex aspect-video flex-col items-center justify-center gap-1">
<span className="text-xl font-semibold text-text-primary">
{threat.count}
</span>
<span className="text-sm text-text-secondary">
{threat.name}
</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Usage
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@zenoxai/ui/components/carousel"
<Carousel aria-label="Threats this week">
<CarouselContent>
<CarouselItem aria-label="1 of 3">...</CarouselItem>
<CarouselItem aria-label="2 of 3">...</CarouselItem>
<CarouselItem aria-label="3 of 3">...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
Use a carousel when items are browsed one or a few at a time and their order matters little: evidence screenshots, report templates, onboarding cards. It hides most of its content, so do not put information there that the user must see. For a list the user scans or compares, use a grid or a Scroll Area.
The component wraps Embla Carousel. Pass Embla options
through opts and plugins (autoplay, wheel gestures) through plugins. From the md
breakpoint (768px), CarouselPrevious and CarouselNext sit outside the slides, 48px to each
side: leave that room around the carousel, or position them yourself with className. On
smaller screens they move inside the edges, 8px in, over the slides, so they stay on screen
and within reach; change that placement with max-md: classes.
Examples
Several slides per view
Set the width of each slide with a basis-* class on CarouselItem. With
opts={{ align: "start" }} the slides align to the start of the row instead of the center.
function CarouselMultiple() {
const reports = [
"Weekly exposure",
"Monthly exposure",
"Brand abuse",
"Takedown status",
"Executive summary",
"Vulnerability trends",
]
return (
<Carousel
aria-label="Report templates"
opts={{ align: "start" }}
className="w-full max-w-md"
>
<CarouselContent>
{reports.map((report, index) => (
<CarouselItem
key={report}
aria-label={`${index + 1} of ${reports.length}`}
className="basis-1/2 sm:basis-1/3"
>
<Card>
<CardContent className="flex aspect-square items-center justify-center text-center text-sm font-medium text-text-primary">
{report}
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Reading the position
setApi hands you the Embla API. Subscribe to its select event to show the current slide or
to drive your own controls.
function CarouselWithApi() {
const [api, setApi] = React.useState<CarouselApi>()
const [current, setCurrent] = React.useState(0)
const captures = [
"21 Oct, 09:14",
"21 Oct, 15:40",
"22 Oct, 08:02",
"23 Oct, 11:27",
]
React.useEffect(() => {
if (!api) return
const onSelect = () => setCurrent(api.selectedScrollSnap())
api.on("select", onSelect)
return () => {
api.off("select", onSelect)
}
}, [api])
return (
<div className="flex w-full max-w-xs flex-col items-center gap-2">
<Carousel
aria-label="Screenshots of acme-login.co"
setApi={setApi}
className="w-full"
>
<CarouselContent>
{captures.map((capture, index) => (
<CarouselItem
key={capture}
aria-label={`${index + 1} of ${captures.length}`}
>
<Card>
<CardContent className="flex aspect-video items-center justify-center text-sm text-text-secondary">
Captured {capture}
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<span className="text-sm text-text-muted" aria-live="polite">
Screenshot {current + 1} of {captures.length}
</span>
</div>
)
}Accessibility
Carouselis a region witharia-roledescription="carousel", and eachCarouselItema group witharia-roledescription="slide". Give the carousel anaria-labeland each slide a position label such as “2 of 5”.- Left and Right arrow keys move between slides while focus is inside the carousel. The
buttons are disabled at either end, unless
opts={{ loop: true }}. - The buttons carry the English labels “Previous slide” and “Next slide”. Pass a translated
aria-labelwhen the interface is not in English. - Do not autoplay. If a plugin does, give the user a control to pause it.
API reference
Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.
Carousel
Carousel shows a row of slides one view at a time, moved with the arrow buttons, a swipe or the arrow keys; it is built on Embla. Use it for media and previews, not for content the user must read in full: hidden slides are easy to miss.
| Prop | Type | Default |
|---|---|---|
opts | Partial<OptionsType>Embla options, such as `loop` or `align`; `axis` follows `orientation`. | - |
orientation | "horizontal" | "vertical"Scroll direction. Defaults to horizontal. | horizontal |
plugins | CreatePluginType<LoosePluginType, {}>[]Embla plugins, such as autoplay. | - |
setApi | ((api: EmblaCarouselType) => void)Receives the Embla API when ready, to drive the carousel from outside. | - |
CarouselContent
CarouselContent is the scrolling track that holds the CarouselItems.
No props of its own. Accepts the props of the element it renders.
CarouselItem
CarouselItem is one slide. It spans the whole track by default; set a basis class such as `basis-1/3` to show several at once.
No props of its own. Accepts the props of the element it renders.
CarouselPrevious
CarouselPrevious scrolls back one slide and is disabled at the start. Its accessible name is the English "Previous slide"; pass a translated aria-label.
| Prop | Type | Default |
|---|---|---|
focusableWhenDisabled | booleanWhether the button should be focusable when disabled. | false |
nativeButton | booleanWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | true |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullHeight: `xs` 24px, `sm` 28px, `default` 32px, `lg` 36px. The `icon-*` sizes are square buttons of the same heights for a single icon. The icon follows the size name: 12px at `xs`, 14px at `sm`, 16px at `default` and `lg`. | icon-sm |
variant | "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | nullVisual weight. `default` is the filled primary action, one per view; `outline` and `secondary` are for the other actions; `ghost` has no fill until hover, for toolbars and icon buttons; `destructive` is for delete and other irreversible actions; `link` looks like inline text. | outline |
CarouselNext
CarouselNext scrolls forward one slide and is disabled at the end. Its accessible name is the English "Next slide"; pass a translated aria-label.
| Prop | Type | Default |
|---|---|---|
focusableWhenDisabled | booleanWhether the button should be focusable when disabled. | false |
nativeButton | booleanWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | true |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | nullHeight: `xs` 24px, `sm` 28px, `default` 32px, `lg` 36px. The `icon-*` sizes are square buttons of the same heights for a single icon. The icon follows the size name: 12px at `xs`, 14px at `sm`, 16px at `default` and `lg`. | icon-sm |
variant | "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | nullVisual weight. `default` is the filled primary action, one per view; `outline` and `secondary` are for the other actions; `ghost` has no fill until hover, for toolbars and icon buttons; `destructive` is for delete and other irreversible actions; `link` looks like inline text. | outline |
useCarousel
useCarousel returns the Embla API and scroll state of the Carousel above.
No props of its own. Accepts the props of the element it renders.