WebberUI

Cover Carousel

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

Loading preview…
npx shadcn@latest add https://webberui.com/r/cover-carousel.json

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
<CoverCarousel />

Installation

npx shadcn@latest add https://webberui.com/r/cover-carousel.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
childrenReactNodePut CoverCarouselItems here, one per slide
indexnumberCurrent index in controlled mode; changing it scrolls to that slide
defaultIndexnumber0Initial index in uncontrolled mode
onIndexChange(index: number) => voidFires 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"輪播"Accessible label of the carousel region — the built-in default is Traditional Chinese for "carousel", so pass this prop to localize it
classNamestringThe outer region
viewportClassNamestringThe horizontal scroll viewport

CoverCarouselItem

PropTypeDefaultDescription
childrenReactNodeSlide content
aria-labelstringautomaticOverrides this slide's accessible label; defaults to "slide N of M"
classNamestringSets 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="輪播" (Traditional Chinese for "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.

On this page