- Accordion
- Alert
- Alert Dialog
- Avatar
- Badge
- Button
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Circular Progress
- Collapsible
- Compare Slider
- Dialog
- Dropdown Menu
- Gauge
- Input
- Label
- Marquee
- Navigation Menu
- Number Field
- Pagination
- Popover
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Tooltip
Demos
One slide per view, with previous and next buttons.
"use client"
import { Card, CardContent } from "@workspace/ui/components/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@workspace/ui/components/carousel"
const markers = [
{ name: "Vitamin D", value: "32", unit: "ng/mL" },
{ name: "Ferritin", value: "86", unit: "ng/mL" },
{ name: "HbA1c", value: "5.2", unit: "%" },
{ name: "LDL", value: "104", unit: "mg/dL" },
{ name: "hs-CRP", value: "0.8", unit: "mg/L" },
]
export function CarouselDemo() {
return (
<Carousel className="w-full max-w-xs">
<CarouselContent>
{markers.map((marker) => (
<CarouselItem key={marker.name}>
<Card>
<CardContent className="flex aspect-square flex-col items-center justify-center gap-2">
<span className="text-sm text-muted-foreground">
{marker.name}
</span>
<span className="text-4xl font-semibold tabular-nums">
{marker.value}
</span>
<span className="text-sm text-muted-foreground">
{marker.unit}
</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Usage
Use a carousel to browse a short set of related items when space is tight, such as biomarker highlights or onboarding steps. Do not hide content the reader must see. Slides past the first are often missed, so put the most important item first. If the reader needs to compare items, use a grid instead.
Import it from @workspace/ui/components/carousel. It is built on Embla Carousel, so every Embla option works through opts.
Anatomy
- Carousel — The root. Holds the Embla instance and handles arrow keys.
- CarouselContent — The viewport and the track that slides.
- CarouselItem — One slide. Full width by default.
- CarouselPrevious — Button that moves back one slide. Disabled at the start.
- CarouselNext — Button that moves forward one slide. Disabled at the end.
Notes
- The buttons sit outside the carousel, 48px from each edge. Leave room for them, or move them with
className. - Set slide width with a
basis-*class onCarouselItem, for examplebasis-1/3. - Spacing between slides comes from
ps-4on each item and-ms-4on the track. Change both together. - For a vertical carousel, give
CarouselContenta fixed height. - Use
useCarousel()inside a child to build custom controls.
Examples
Multiple per view
Use basis-1/2 or basis-1/3 on each item to show more than one slide. opts={{ align: "start" }} lines slides up with the left edge.
"use client"
import { Card, CardContent } from "@workspace/ui/components/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@workspace/ui/components/carousel"
const panels = [
"Heart",
"Metabolic",
"Thyroid",
"Liver",
"Kidney",
"Hormones",
"Nutrients",
]
export function CarouselSizes() {
return (
<Carousel opts={{ align: "start" }} className="w-full max-w-sm">
<CarouselContent>
{panels.map((panel) => (
<CarouselItem key={panel} className="basis-1/2 md:basis-1/3">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-4">
<span className="text-sm font-medium">{panel}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
With slide counter
Pass setApi to get the Embla API. Listen for select to track the current slide.
Step 0 of 0
"use client"
import * as React from "react"
import { Card, CardContent } from "@workspace/ui/components/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
} from "@workspace/ui/components/carousel"
const steps = [
"Book a blood draw",
"Fast for 10 to 12 hours",
"Visit the lab",
"Get results in 48 hours",
]
export function CarouselApiDemo() {
const [api, setApi] = React.useState<CarouselApi>()
const [current, setCurrent] = React.useState(0)
const [count, setCount] = React.useState(0)
React.useEffect(() => {
if (!api) return
const update = () => {
setCount(api.scrollSnapList().length)
setCurrent(api.selectedScrollSnap() + 1)
}
update()
api.on("select", update)
api.on("reInit", update)
return () => {
api.off("select", update)
api.off("reInit", update)
}
}, [api])
return (
<div className="w-full max-w-xs">
<Carousel setApi={setApi}>
<CarouselContent>
{steps.map((step) => (
<CarouselItem key={step}>
<Card>
<CardContent className="flex aspect-video items-center justify-center p-6 text-center">
<span className="text-lg font-medium">{step}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<p
className="mt-3 text-center text-sm text-muted-foreground"
aria-live="polite"
>
Step {current} of {count}
</p>
</div>
)
}
Vertical
Set orientation="vertical". The buttons move above and below.
"use client"
import { Card, CardContent } from "@workspace/ui/components/card"
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@workspace/ui/components/carousel"
const tips = [
"Drink water before your draw.",
"Skip intense exercise the day before.",
"Bring a list of your supplements.",
"Wear a top with loose sleeves.",
]
export function CarouselVertical() {
return (
<Carousel
orientation="vertical"
opts={{ align: "start" }}
className="my-12 w-full max-w-xs"
>
<CarouselContent className="h-[220px]">
{tips.map((tip) => (
<CarouselItem key={tip} className="basis-1/2">
<Card>
<CardContent className="flex items-center justify-center p-6 text-center">
<span className="text-sm font-medium">{tip}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
Props
These are the props you set most often. Unlisted props pass through to the underlying element.
Carousel
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Direction the slides move. |
opts | EmblaOptionsType | — | Embla options, such as align and loop. |
plugins | EmblaPluginType[] | — | Embla plugins, such as autoplay. |
setApi | (api: CarouselApi) => void | — | Receives the Embla API once it is ready. |
className | string | — | Classes merged onto the element. |
CarouselPrevious / CarouselNext
| Prop | Type | Default | Description |
|---|---|---|---|
variant | Button variant | "outline" | Button style. |
size | Button size | "icon-sm" | Button size. |
className | string | — | Classes merged onto the button. |