RadCN
Compositereadycomponentready

Carousel

A browser-enhanced scroll carousel with native region and slide semantics, controls, vertical layout, responsive sizing, status hooks, and app-owned autoplay composition.

Importimport { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from 'radcn/carousel'
Preview

Live package example

Render Demo, Size, Spacing, Orientation, API, and Plugin-style examples with Card slides, public events, and app-owned behavior.

Example Parity

Render Demo, Size, Spacing, Orientation, API, and Plugin-style examples with Card slides, public events, and app-owned behavior.

Preview
import { Card, CardContent } from 'radcn/card'
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from 'radcn/carousel'

const slides = ['1', '2', '3', '4', '5']

function SlideCard({ label }: { label: string }) {
  return (
    <Card class="radcn-carousel-slide-card">
      <CardContent style="display:grid;min-height:inherit;place-items:center;padding:1rem;">
        <span>{label}</span>
      </CardContent>
    </Card>
  )
}

function SlideItems() {
  return slides.map((slide, index) => (
    <CarouselItem ariaLabel={`Slide ${index + 1} of ${slides.length}`} index={index} selected={index === 0}>
      <SlideCard label={slide} />
    </CarouselItem>
  ))
}

export function CarouselPreview() {
  return (
    <div class="carousel-preview">
      <Carousel class="radcn-carousel--demo" ariaLabel="Featured slides">
        <CarouselContent><SlideItems /></CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>

      <div data-carousel-example>
        <Carousel class="radcn-carousel--api" ariaLabel="API slides">
          <CarouselContent><SlideItems /></CarouselContent>
          <CarouselPrevious />
          <CarouselNext />
        </Carousel>
        <div class="radcn-carousel-status" data-carousel-status>Slide 1 of 5</div>
      </div>

      <Carousel class="radcn-carousel--size" ariaLabel="Responsive slides">
        <CarouselContent><SlideItems /></CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>

      <Carousel class="radcn-carousel--spacing" ariaLabel="Compact slides">
        <CarouselContent><SlideItems /></CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>

      <Carousel class="radcn-carousel--orientation" ariaLabel="Vertical slides" orientation="vertical">
        <CarouselContent><SlideItems /></CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>

      <div data-carousel-autoplay data-carousel-delay="2000">
        <Carousel class="radcn-carousel--plugin" ariaLabel="Autoplay slides">
          <CarouselContent><SlideItems /></CarouselContent>
          <CarouselPrevious />
          <CarouselNext />
        </Carousel>
        <div class="radcn-carousel-plugin-note">Autoplay pauses on hover.</div>
      </div>
    </div>
  )
}

export function enhanceCarouselStatus(root: ParentNode = document) {
  root.querySelectorAll<HTMLElement>('[data-carousel-example]').forEach((example) => {
    let carousel = example.querySelector<HTMLElement>('[data-radcn-carousel]')
    let status = example.querySelector<HTMLElement>('[data-carousel-status]')
    if (!carousel || !status) return
    let sync = () => {
      status.textContent = 'Slide ' + (carousel.dataset.current || '1') + ' of ' + (carousel.dataset.count || '0')
    }
    carousel.addEventListener('radcn-carousel-select', sync)
    carousel.addEventListener('radcn-carousel-scroll', sync)
    sync()
  })
}

export function enhanceCarouselAutoplay(root: ParentNode = document) {
  root.querySelectorAll<HTMLElement>('[data-carousel-autoplay]').forEach((example) => {
    let next = example.querySelector<HTMLButtonElement>('[data-radcn-carousel-next]')
    if (!next) return
    let delay = Number(example.dataset.carouselDelay || '2000')
    let timer = window.setInterval(() => next.click(), delay)
    example.addEventListener('mouseenter', () => window.clearInterval(timer))
    example.addEventListener('mouseleave', () => {
      timer = window.setInterval(() => next.click(), delay)
    })
  })
}

Installation

Intended future install command. RadCN is private and not published to npm yet, so this snippet documents the target user-facing API rather than something external consumers can run today.

pnpm add radcn # intended future package
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from 'radcn/carousel'

Theming

RadCN tokens read the resolved theme from the document. Store the user's preference separately, then resolve system preferences to a concrete light or dark theme before setting package tokens.

<html data-radcn-theme-mode="system" data-radcn-theme="dark">
  ...
</html>

Accessibility

  • Carousel renders a named region with aria-roledescription="carousel" and each item renders a slide group.
  • CarouselItem can provide ariaLabel or ariaLabelledby so numbered Card slides have deterministic accessible names.
  • Previous and Next are native buttons with stable accessible labels and disabled boundary states.
  • Enhanced carousels expose current/count data hooks and selection events so app-owned status text can remain visible and synchronized.
  • Keyboard movement uses the active axis: left/right for horizontal, up/down for vertical, plus Home and End.

Customization

  • Use class, style, and CSS variables such as --radcn-carousel-width, --radcn-carousel-gap, --radcn-carousel-item-size, and --radcn-carousel-vertical-height to tune layout.
  • Card slides are ordinary composition; Carousel does not require or own Card.
  • Responsive size and spacing examples map to public classes and CSS variables instead of Tailwind basis or negative-margin utilities.
  • Autoplay/plugin behavior can be app-owned browser enhancement over public controls and data hooks.

Remix 3 Notes

  • React state/effects/context and shadcn setApi map to public data hooks, radcn-carousel-select events, radcn-carousel-scroll events, and app-owned browser state.
  • Embla, useEmblaCarousel, opts, plugins, and embla-carousel-autoplay are upstream implementation mechanics, not RadCN dependencies.
  • Lucide arrows are presentation choices; RadCN controls use package-owned glyphs or app-owned children with accessible button labels.
  • Tailwind width, max-width, aspect-square, padding, negative margin, basis, height, and responsive utilities map to RadCN classes, inline styles, or CSS variables.