WebberUI

Perspective Carousel

A 3D perspective carousel (coverflow style) with drag to page, looping, autoplay, and controlled and uncontrolled modes.

Loading preview…
npx shadcn@latest add https://webberui.com/r/perspective-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.

1200
45
120
2
0.12
<PerspectiveCarousel />

Installation

npx shadcn@latest add https://webberui.com/r/perspective-carousel.json

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

Usage

import { PerspectiveCarousel } from "@/components/ui/perspective-carousel";

const items = [
  { id: "a", content: <img src="/1.jpg" alt="" className="h-full w-full object-cover" /> },
  { id: "b", content: <img src="/2.jpg" alt="" className="h-full w-full object-cover" /> },
  { id: "c", content: <img src="/3.jpg" alt="" className="h-full w-full object-cover" /> },
];

<PerspectiveCarousel items={items} loop />

Controlled mode

Passing index and onIndexChange switches to controlled mode, so it can stay in sync with outside state (indicator dots, a thumbnail strip, and so on):

const [active, setActive] = React.useState(0);

<PerspectiveCarousel items={items} index={active} onIndexChange={setActive} loop />

Programmatic control

Get next / prev / goTo through the ref:

const ref = React.useRef<PerspectiveCarouselHandle>(null);

<PerspectiveCarousel ref={ref} items={items} />
<button onClick={() => ref.current?.next()}>Next</button>

Props

PropTypeDefaultDescription
itemsPerspectiveCarouselItem[]The array of slides, each with an id and content
indexnumberControlled current index (0-based); when provided, control sits outside
defaultIndexnumber0Initial index in uncontrolled mode
onIndexChange(index: number) => voidFires when the index changes
loopbooleanfalseWhether to loop (a ring-shaped 3D arrangement joining head to tail)
itemWidthnumber240Slide width (px)
itemHeightnumber320Slide height (px)
visiblenumber2Number of neighboring slides visible on each side
perspectivenumber1200CSS perspective distance (px); the smaller it is, the stronger the perspective
rotatenumber45Rotation of neighboring slides about the Y axis (deg)
depthnumber120Z offset pushed back per step (px)
spreadnumberitemWidth * 0.6Horizontal spacing per step (px)
scaleStepnumber0.12Scale reduction per step
autoplaybooleanfalseAutoplay (disabled under reduce motion)
autoplayIntervalnumber3500Autoplay interval (ms)
showArrowsbooleantrueWhether to show the left and right arrows
velocityThresholdnumber320Flick velocity threshold that triggers paging (px/s)
ariaLabelstring"3D 透視輪播"Accessible label of the carousel region — the built-in default is Traditional Chinese for "3D perspective carousel", so pass this prop to localize it

Types

interface PerspectiveCarouselItem {
  id: string | number;
  content: React.ReactNode;
}

interface PerspectiveCarouselHandle {
  next: () => void;
  prev: () => void;
  goTo: (index: number) => void;
}

How it works

  • Coverflow perspective is built with pure CSS 3D transforms (perspective + rotateY + translateZ), with no WebGL library involved.
  • Every slide shares one floating-point position motion value, and the offset, rotation, scale, opacity, and dimming from the center outward are all mapped continuously from that value, so the drag tracks the pointer in real time.
  • On release it pages by rounding to the nearest landing point, and if the flick velocity exceeds velocityThreshold it advances one more slide in the direction of the flick.
  • With loop on, it settles along the shortest ring path and normalizes the position once the animation ends, so drift does not accumulate over a long run.

Accessibility

  • The carousel container carries role="group", aria-roledescription="輪播" (Traditional Chinese for "carousel"), and a focusable tabIndex; the arrow keys page through, and Home / End jump straight to the first and last slide.
  • The current slide number is announced through a hidden region with aria-live="polite" ("slide N of M"); slides other than the current one are marked aria-hidden.
  • The left and right arrows are native buttons with an aria-label, and they are disabled at the boundaries (when not looping).
  • When the user has "reduce motion" enabled at the system level, the drag and the autoplay are disabled and paging switches instantly with no tween animation.
  • Autoplay pauses automatically when the pointer hovers or focus enters the carousel region.

On this page