Unified UI

Carousel

A horizontal or vertical carousel with animated slide transitions, navigation arrows, dot indicators, and optional autoplay. Built with Framer Motion.

Basic

A fully-featured carousel with animated slide transitions, previous/next arrows, dot indicators, autoplay, loop mode, and keyboard-accessible controls.

Slide 1

Installation

Install the component via the CLI in one command.

npx @work-rjkashyap/unified-ui add carousel
pnpm dlx @work-rjkashyap/unified-ui add carousel
npx @work-rjkashyap/unified-ui add carousel
bunx @work-rjkashyap/unified-ui add carousel

If you haven't initialized your project yet, run npx @work-rjkashyap/unified-ui init first. See the CLI docs for details.

Or install the full package

Use this approach if you prefer to install the entire design system as a dependency instead of copying individual components.

npm install @work-rjkashyap/unified-ui
pnpm add @work-rjkashyap/unified-ui
yarn add @work-rjkashyap/unified-ui
bun add @work-rjkashyap/unified-ui

Anatomy

import {
	Carousel,
	Button,
	Card,
	Badge,
} from "@work-rjkashyap/unified-ui";

Basic Usage

Pass an array of ReactNode elements to items. The carousel manages its own internal index by default (uncontrolled).

<Carousel
  items={[
    <img src="/slide-1.jpg" alt="Slide 1" className="w-full rounded-lg" />,
    <img src="/slide-2.jpg" alt="Slide 2" className="w-full rounded-lg" />,
    <img src="/slide-3.jpg" alt="Slide 3" className="w-full rounded-lg" />,
  ]}
/>

A common pattern — cycling through card content.

RJ

Rajeshwar Kashyap

Creator

"Unified UI makes building consistent design systems effortless. The token system is exceptional."

Autoplay

Set autoplay to automatically advance slides at a configurable interval. Autoplay pauses when the user interacts with the carousel.

Slide 1 — Auto
<Carousel
  autoplay
  autoplayInterval={3000}
  loop
  items={[
    <SlideA />,
    <SlideB />,
    <SlideC />,
  ]}
/>

Without Arrows or Dots

Disable navigation controls for minimal, gesture-only carousels.

No arrows, dots only

Slide 1

Arrows only, no dots

Slide 1

No Loop

Set loop={false} to disable wrapping — arrow buttons are disabled at the first and last slides.

<Carousel
  loop={false}
  items={[
    <SlideA />,
    <SlideB />,
    <SlideC />,
  ]}
/>

Controlled

Use index and onIndexChange for external control of the active slide.

const [current, setCurrent] = useState(0);
const total = slides.length;

<div className="flex flex-col gap-4">
  <Carousel
    index={current}
    onIndexChange={setCurrent}
    items={slides}
    showDots={false}
  />

  {/* External step indicator */}
  <div className="flex items-center justify-between text-sm text-muted-foreground">
    <Button
      variant="ghost"
      size="sm"
      onClick={() => setCurrent((c) => Math.max(0, c - 1))}
      disabled={current === 0}
    >
      ← Previous
    </Button>
    <span>
      {current + 1} / {total}
    </span>
    <Button
      variant="ghost"
      size="sm"
      onClick={() => setCurrent((c) => Math.min(total - 1, c + 1))}
      disabled={current === total - 1}
    >
      Next →
    </Button>
  </div>
</div>

Default Index

Set the starting slide with defaultIndex.

{/* Start on the third slide */}
<Carousel
  defaultIndex={2}
  items={[<SlideA />, <SlideB />, <SlideC />, <SlideD />]}
/>

Item Class

Use itemClassName to apply additional classes to each slide wrapper — useful for setting aspect ratios, min-heights, or padding.

<Carousel
  itemClassName="aspect-video"
  items={[
    <img src="/photo-1.jpg" className="w-full h-full object-cover rounded-lg" />,
    <img src="/photo-2.jpg" className="w-full h-full object-cover rounded-lg" />,
    <img src="/photo-3.jpg" className="w-full h-full object-cover rounded-lg" />,
  ]}
/>

A typical use case for product landing pages.

const features = [
  {
    title: "Token-Driven Design",
    description: "Every color, spacing value, and radius comes from CSS custom properties.",
    icon: <Layers className="size-6" />,
    color: "text-primary",
  },
  {
    title: "Motion Presets",
    description: "15+ Framer Motion presets for entrance, hover, and interaction animations.",
    icon: <Zap className="size-6" />,
    color: "text-success",
  },
  {
    title: "Fully Accessible",
    description: "Built on Radix UI primitives with WCAG AA keyboard and ARIA support.",
    icon: <Shield className="size-6" />,
    color: "text-info",
  },
];

<Carousel
  loop
  autoplay
  autoplayInterval={4000}
  items={features.map((feature) => (
    <div
      key={feature.title}
      className="flex flex-col items-center text-center gap-4 py-10 px-8"
    >
      <div className={cn("p-3 rounded-xl bg-muted", feature.color)}>
        {feature.icon}
      </div>
      <h3 className="text-lg font-semibold">{feature.title}</h3>
      <p className="text-sm text-muted-foreground max-w-xs">
        {feature.description}
      </p>
    </div>
  ))}
/>
const photos = [
  { src: "/gallery/1.jpg", alt: "Mountain vista at sunrise" },
  { src: "/gallery/2.jpg", alt: "Forest path in autumn" },
  { src: "/gallery/3.jpg", alt: "Ocean waves at dusk" },
  { src: "/gallery/4.jpg", alt: "City skyline at night" },
];

<Carousel
  itemClassName="aspect-video"
  loop
  items={photos.map((photo) => (
    <img
      key={photo.src}
      src={photo.src}
      alt={photo.alt}
      className="w-full h-full object-cover rounded-lg"
    />
  ))}
/>

Onboarding Stepper

Combine with controlled mode for a linear onboarding flow.

const steps = [
  {
    step: 1,
    title: "Connect your repository",
    description: "Link your GitHub, GitLab, or Bitbucket account to get started.",
  },
  {
    step: 2,
    title: "Configure your project",
    description: "Select your framework, set environment variables, and define build settings.",
  },
  {
    step: 3,
    title: "Deploy",
    description: "Your first deployment is triggered automatically after configuration.",
  },
];

function OnboardingFlow() {
  const [step, setStep] = useState(0);

  return (
    <div className="flex flex-col gap-6 max-w-lg mx-auto">
      {/* Progress */}
      <div className="flex gap-2">
        {steps.map((s, i) => (
          <div
            key={i}
            className={cn(
              "flex-1 h-1 rounded-full transition-colors",
              i <= step ? "bg-primary" : "bg-muted"
            )}
          />
        ))}
      </div>

      {/* Slides */}
      <Carousel
        index={step}
        onIndexChange={setStep}
        showArrows={false}
        showDots={false}
        loop={false}
        items={steps.map((s) => (
          <div key={s.step} className="flex flex-col gap-3 px-2 py-6">
            <p className="text-xs font-medium text-muted-foreground">
              Step {s.step} of {steps.length}
            </p>
            <h2 className="text-xl font-bold">{s.title}</h2>
            <p className="text-sm text-muted-foreground">{s.description}</p>
          </div>
        ))}
      />

      {/* Controls */}
      <div className="flex justify-between">
        <Button
          variant="ghost"
          size="sm"
          onClick={() => setStep((s) => s - 1)}
          disabled={step === 0}
        >
          Back
        </Button>
        <Button
          variant="primary"
          size="sm"
          onClick={() => step < steps.length - 1
            ? setStep((s) => s + 1)
            : handleComplete()
          }
        >
          {step === steps.length - 1 ? "Finish" : "Next →"}
        </Button>
      </div>
    </div>
  );
}

Props

PropTypeDefaultDescription
itemsReactNode[]Required. Array of slide content to render.
defaultIndexnumber0Initial slide index for uncontrolled mode.
indexnumberControlled slide index.
onIndexChange(index: number) => voidCallback fired when the active slide changes.
orientation"horizontal" | "vertical""horizontal"Slide transition direction.
autoplaybooleanfalseAutomatically advance slides.
autoplayIntervalnumber3000Milliseconds between auto-advances.
loopbooleantrueWhether to wrap from last slide to first (and vice versa).
showArrowsbooleantrueWhether to show previous/next arrow buttons.
showDotsbooleantrueWhether to show dot navigation indicators.
classNamestringAdditional CSS classes on the root container.
itemClassNamestringCSS classes applied to each slide wrapper element.

Motion

  • Slide transition — Each slide animates in from 100% (right/bottom) and exits to -100% (left/top) using AnimatePresence mode="wait". Direction reverses when navigating backwards.
  • Reduced motion — When prefers-reduced-motion is active, slides use a simple opacity cross-fade instead of positional transforms.
  • Transition: duration: 0.35s, ease: [0.4, 0, 0.2, 1] (standard easing).

Accessibility

  • Root element has aria-roledescription="carousel" and aria-label="Content carousel".
  • Each active slide has aria-roledescription="slide" and aria-label="Slide N of M".
  • Previous/Next arrow buttons have descriptive aria-label attributes.
  • Dot buttons have aria-label="Go to slide N" and aria-current="true" for the active dot.
  • Arrow buttons are disabled (and not just visually hidden) when loop={false} and at the boundary slides.
  • All interactive controls are keyboard accessible via Tab + Enter/Space.
  • For autoplay carousels, consider adding a pause button accessible to keyboard and screen reader users, or pausing on focus.

Design Tokens

TokenUsage
--backgroundArrow button and dot background
--borderArrow button border
--foregroundArrow icon color
--radius-fullArrow button and dot border radius
--radius-lgCarousel container border radius
--shadow-smArrow button shadow
--duration-fastArrow button hover transition

On this page