Skip to content

Carousel

Browse slides by swiping, dragging or stepping through controls, with optional autoplay.

import { Carousel, CarouselAutoplayToggle, CarouselCaption, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious, CarouselThumb, CarouselThumbs } from '@youj/react/carousel'

Installation & usage

npm i @youj/react embla-carousel-react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Requires embla-carousel-react, an optional dependency. Import it from @youj/react/carousel.

It's shadcn's carousel on Embla, so swipe, drag, flick and looping use Embla's physics.

Use a carousel when there are more things than fit and seeing one at a time is the point: product photos, a gallery, a few featured stories. Everything after the first slide is easy to miss, so don't put anything people must see on slide three. For content people should scan and compare, lay it out in a grid or a Scroll Area instead.

import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@youj/react/carousel'

export function Photos({ photos }: { photos: Photo[] }) {
  return (
    <Carousel aria-label="Product photos" opts={{ loop: true }}>
      <CarouselContent>
        {photos.map((photo) => (
          <CarouselItem key={photo.id}>
            <img src={photo.src} alt={photo.alt} />
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}

opts goes to Embla (loop, align, slidesToScroll, dragFree…). setApi hands you Embla's API, so anything can drive the carousel: the color swatches in the demo call api.scrollTo(i). onSlideChange reports the current slide however it changed. The Previous and Next buttons sit outside the slides by default; move them inside with a class, as the demo does.

Reference

Propsopts, plugins, orientation… (8)

Props

Carousel props
PropType
optsCarouselOptions–
pluginsCarouselPlugin–
orientation'horizontal' | 'vertical'

default 'horizontal'

'horizontal'
setApi(api: CarouselApi) => void–
autoplayboolean | number

Advance on a timer: true for the default dwell (5000), or the dwell per slide in milliseconds. Pauses on hover, focus and drag; never starts under reduced motion.

default false

false
onSlideChange(index: number) => void

Called when the current slide changes, by any means.

–

CarouselThumb

CarouselThumb props
PropType
indexRequirednumber

The snap this thumbnail shows.

–
childrenRequiredReactNode–
Other props pass through: extends ComponentProps<'button'>.

CarouselContent, CarouselItem, CarouselCaption, CarouselDots and CarouselThumbs are styled divs. CarouselPrevious, CarouselNext and CarouselAutoplayToggle take Button props, with variant and size defaulting to outline and icon-sm (ghost for the toggle). useCarousel() reads the carousel's state from inside it.

MotionSlide travel, drag and flick, dot and thumbnail indicator… · reduced motion

Motion

Motion tokens
MomentTokens
Slide travelbuttons, dots, thumbnails and arrow keys; Embla's integrator is tuned to land on the token
duration.slow

340ms

340ms
Drag and flickfollows the finger, then Embla's own momentum
no token

pointer-driven

pointer-driven
Dot and thumbnail indicatorglides to the current slide (SlidingIndicator)
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Captionrises 8px and fades in after a loose stagger as its slide arrives
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Caption leaves
duration.instantease.exit

80ms · cubic-bezier(0.4, 0, 1, 1)

80mscubic-bezier(0.4, 0, 1, 1)
Autoplay ringdraws the dwell; it is the timer
no token

the autoplay interval

the autoplay interval

Reduced motion

Buttons, dots and keys jump to the slide; drags still follow the finger. Captions are simply there, and autoplay doesn't start.

A Speed change re-initializes Embla with the new travel, so the Speed control reaches the slides too.

Reduced motion

Buttons, dots, thumbnails and keys jump straight to the slide, captions don't rise and autoplay doesn't start on its own. Dragging still follows the finger, because that movement is the person's own.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The carousel is a region with aria-roledescription="carousel". Name it with aria-label.
  • Each slide is a group with aria-roledescription="slide", labeled with its position ("3 of 8") unless you give it an aria-label of your own, such as "Moss, 2 of 5".
  • With focus inside the carousel (on a slide's control or the arrows), use Left and Right (Up and Down when vertical), Home and End. The arrows flip under RTL, and keys typed into a field inside a slide are left alone.
  • A polite live region names the slide that's showing. It's silent while autoplay rotates, so it doesn't talk over the page every few seconds (WAI-ARIA APG).
  • Previous and Next stay focusable when disabled at either end, so focus isn't dropped onto the page.
  • Dots are buttons named "Slide 3", with aria-current on the current one, and each has a 24px hit area.