Carousel
The Carousel component provides a flexible UI element for stepping through a set of slides one at a time. It’s built on Embla Carousel and composed from Carousel.Content, Carousel.Item, Carousel.Previous, and Carousel.Next, so you assemble exactly the structure you need.
Variations
Vertical
Set orientation="vertical" to scroll slides up and down instead of left to right. Give Carousel.Content a fixed height via carouselBlockClassName so the viewport can clip to one slide.
Installation
import { Carousel } from '@harnessio/ui/components'
return ( <Carousel.Root> <Carousel.Previous /> <Carousel.Content> <Carousel.Item> <div className="h-64 w-full bg-cn-blue-primary text-cn-1 flex items-center justify-center">Slide 1</div> </Carousel.Item> <Carousel.Item> <div className="h-64 w-full bg-cn-success-primary text-cn-1 flex items-center justify-center">Slide 2</div> </Carousel.Item> <Carousel.Item> <div className="h-64 w-full bg-cn-danger-primary text-cn-1 flex items-center justify-center">Slide 3</div> </Carousel.Item> </Carousel.Content> <Carousel.Next /> </Carousel.Root>)Anatomy
All parts of the Carousel are imported from the Carousel object and composed together:
<Carousel.Root> <Carousel.Previous /> <Carousel.Content> <Carousel.Item>{/* slide */}</Carousel.Item> </Carousel.Content> <Carousel.Next /></Carousel.Root>- Root — the container. It sets up the Embla instance, shares it through context, and renders a positioned
role="region"wrapper. It also listens for ←/→ to move between slides. Configure it withorientation,opts(Embla options),plugins,initialSlide, andsetApi(to grab the Embla API for custom controls). - Content — the viewport. It renders an
overflow-hiddenframe (styled viacarouselBlockClassName) wrapping the flex track that holds the items.classNametargets the track itself. - Item — a single slide. Each item is
role="group"/aria-roledescription="slide"and takes the full width (or height) of the viewport, so one slide shows at a time. - Previous / Next — icon buttons that step backward and forward. They’re positioned just outside the viewport, disable themselves at the ends, and carry their own accessible labels. They accept any Button prop except
iconOnly,tooltipProps, andignoreIconOnlyTooltip.
The order you place Previous, Content, and Next in is up to you — the buttons are absolutely positioned relative to Root, not laid out inline.
Best practices
Do
- Give
Roota width (and, for vertical carousels, giveContenta height) so the viewport clips cleanly to one slide. - Leave room around the carousel for the Previous/Next buttons — they sit just outside the viewport edges.
- Keep each slide’s content self-contained and roughly equal in size so the frame doesn’t jump between slides.
- Reach for
opts/plugins(Embla) for behaviors like looping or autoplay rather than rebuilding them.
Don’t
- Don’t hide essential content behind a carousel that people might not swipe through — surface anything critical outside it.
- Don’t put horizontally keyboard-navigable controls (a text field, a slider) inside a slide; the carousel intercepts the arrow keys (see the accessibility notes).
- Don’t rely on the carousel alone to convey how many slides there are — pair it with visible position cues when order matters.
Content
- Keep the number of slides small enough that stepping through them stays reasonable.
- If a slide carries a call to action, make sure it’s reachable without depending on auto-advance.
Accessibility
Carousel ships with region and slide semantics and keyboard navigation, and its buttons carry built-in labels.
Keyboard
- With focus anywhere inside the carousel region, ← moves to the previous slide and → to the next.
- The Previous and Next buttons are in the tab order and activate with Enter/Space; each disables itself when there are no more slides in that direction.
Labeling
Rootrendersrole="region"witharia-roledescription="carousel", and eachItemisrole="group"witharia-roledescription="slide".- The Previous and Next buttons show a “Previous”/“Next” tooltip and include visually hidden “Previous slide”/“Next slide” text, so they always have an accessible name.
Focus & states
- The nav buttons use the standard button focus ring and reflect their disabled state at the start and end of the carousel.
Known limitations
- The carousel has no accessible name by default. The region isn’t labeled out of the box.
Rootforwards extra props, so pass anaria-label(oraria-labelledby) to name what the carousel contains. - Slides don’t announce their position. Each slide is described as “slide”, but there’s no “1 of 3” context and no live region announcing when the active slide changes.
- The arrow-key handler is greedy.
Rootcaptures ←/→ and callspreventDefault, so those keys move the carousel even when focus is on interactive content inside a slide. Avoid placing horizontally-navigable controls (text inputs, sliders) in slides, or they’ll fight the carousel for the arrow keys.
Properties
Root
The main container for all carousel elements.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode | |
| orientation | false | 'horizontal' | 'horizontal' | 'vertical' |
| opts | false | CarouselOptions | |
| plugins | false | CarouselPlugin | |
| setApi | false | (api: CarouselApi) => void | |
| initialSlide | false | number | |
| className | false | string |
Carousel.Root also forwards any other HTMLAttributes<HTMLDivElement> (e.g. aria-label) onto the region element.
Content
The viewport that clips to the active slide and holds the item track.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode | |
| className | false | string | |
| carouselBlockClassName | false | string |
Item
An individual slide within the carousel.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode | |
| className | false | string |
Previous
Steps to the previous slide. Accepts any Button prop except iconOnly, tooltipProps, and ignoreIconOnlyTooltip.
Prop | Required | Default | Type |
|---|---|---|---|
| variant | false | 'outline' | ButtonProps['variant'] |
| theme | false | 'default' | ButtonProps['theme'] |
| size | false | 'sm' | ButtonProps['size'] |
| className | false | string |
Next
Steps to the next slide. Accepts any Button prop except iconOnly, tooltipProps, and ignoreIconOnlyTooltip.
Prop | Required | Default | Type |
|---|---|---|---|
| variant | false | 'outline' | ButtonProps['variant'] |
| theme | false | 'default' | ButtonProps['theme'] |
| size | false | 'sm' | ButtonProps['size'] |
| className | false | string |