Carousels

Cover Carousel

A horizontal carousel of cover cards whose indicator is driven continuously (scrubbed) by the scroll ratio, with full keyboard navigation and ARIA.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add cover-carousel.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

16
Active edited usageDefault props
<CoverCarousel/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/cover-carousel.en.json

Or, once registries are configured in components.json, install it as @webberui/cover-carousel.

Usage

Wrap everything in CoverCarousel and put a number of CoverCarouselItems inside; the width of each slide is up to you via className:

import {
  CoverCarousel,
  CoverCarouselItem,
} from "@/components/ui/cover-carousel";

<CoverCarousel ariaLabel="Featured models">
  {items.map((item) => (
    <CoverCarouselItem key={item.id} className="w-[240px]">
      <Card {...item} />
    </CoverCarouselItem>
  ))}
</CoverCarousel>;

Controlled mode: pass index and onIndexChange to drive the current slide from outside.

const [index, setIndex] = React.useState(0);

<CoverCarousel index={index} onIndexChange={setIndex}>
  {/* ... */}
</CoverCarousel>;

Props

CoverCarousel

PropTypeDefaultDescription
childrenReactNode—Put CoverCarouselItems here, one per slide
indexnumber—Current index in controlled mode; changing it scrolls to that slide
defaultIndexnumber0Initial index in uncontrolled mode
onIndexChange(index: number) => void—Fires when the centered slide changes
controlsbooleantrueWhether to show the left and right arrow buttons
indicators"dots" | "bar" | "none""dots"Style of the indicator at the bottom
loopbooleanfalseWhether to wrap around at the start and the end
snapAlign"start" | "center""start"The scroll-snap alignment point of the slides
gapnumber16Spacing between slides (px)
ariaLabelstring"Carousel"Accessible label of the carousel region — the English source includes this default, so pass this prop to localize it
classNamestring—The outer region
viewportClassNamestring—The horizontal scroll viewport

CoverCarouselItem

PropTypeDefaultDescription
childrenReactNode—Slide content
aria-labelstringautomaticOverrides this slide's accessible label; defaults to "slide N of M"
classNamestring—Sets the slide's width and appearance

How it works

  • Scroll scrub: the indicator (the dot pill or the progress bar) is driven continuously by the scroll container's horizontal scroll ratio scrollXProgress and smoothed with useSpring, so while dragging between two slides the pill slides continuously rather than jumping between discrete positions.
  • Alignment detection: scroll events are throttled with requestAnimationFrame, and the slide closest to the viewport's center line is measured as the current index, while the at-start / at-end state is updated at the same time to disable the arrows.
  • Two modes: without index it is uncontrolled (tracking the scroll position internally); pass index and it becomes controlled, so an outside change scrolls to that slide.

Accessibility

  • The whole thing is role="region" with aria-roledescription="Carousel" (meaning "carousel"), and each slide is role="group" labeled "slide N of M".
  • The scroll viewport is focusable, and once focused ← → (or ↑ ↓) switch slides while Home / End jump to the first and the last.
  • The arrows and the dots are all native buttons that can be focused and triggered by keyboard; the dots use role="tab" with aria-selected to mark the current slide.
  • The current slide number is announced through a hidden region with aria-live="polite".
  • When the user turns on "reduce motion", programmatic scrolling positions instantly and the indicator applies no spring smoothing.