Cylinder Carousel
Slides on a rotating 3D ring with buttons, arrow keys, swipe and a pausable autoplay.
pnpm dlx @glinui/cli@latest add cylinder-carouselInstallation
pnpm dlx @glinui/cli@latest add cylinder-carouselCredits. Adapted from Vengeance UI (MIT, (c) 2025-2026 Ashutoshx7). ReactNode items instead of image urls, tokens, keyboard and aria carousel semantics, inert rear slides, autoplay pause, reduced motion, RTL Attribution details.
Prefer the package?
Usage
Geometry is computed in JS (the ring radius from card width, gap and count), so it does not rely on the CSS tan() function.
Autoplay is off by default. When on, it pauses on hover and focus, a visible pause button is shown (WCAG 2.2.2) and it never runs under reduced motion.
Variants
glinr is the default surface, plain is flat, solid is a neutral tonal face.
<CylinderCarousel variant="plain" items={items} cardWidth={140} cardHeight={180} />States
Autoplay pauses on hover and focus, and has a pause button.
<CylinderCarousel autoPlay autoPlayInterval={2800} items={items} />In a layout
A product section with copy beside the ring.
Pick a palette
Spin through curated palettes with the arrows, swipe or the arrow keys.
<section className="grid gap-6 lg:grid-cols-[1fr_1.4fr]"> <Copy /> <CylinderCarousel items={items} /></section>Accessibility
- The root is a region with aria-roledescription carousel. Each slide is a group with aria-roledescription slide and a name such as 3 of 6.
- Only the front slide is interactive. Rear slides are inert so focus and screen readers never land on content that is turned away.
- The viewport is focusable and takes arrow keys. Previous and next are real buttons. Swipe works on touch.
- aria-live is off while autoplay is running and polite otherwise. Autoplay pauses on hover and focus and has a pause button.
Keyboard Interactions
| Key | Action |
|---|---|
| ArrowRight / ArrowLeft | Next or previous slide (reversed in RTL). |
| Home / End | First or last slide. |
| Tab | Moves to the controls and into the front slide. |
ARIA Attributes
- `role="region"` with `aria-roledescription="carousel"`
- `aria-roledescription="slide"` and `aria-label="n of N"` on slides
- `inert` on non-front slides
- `aria-live` off during autoplay
Reduced Motion
Autoplay only runs at motion level full, so reduced motion turns it off. Rotation transitions are dropped at subtle. Motion level none renders a flat scroll-snap list with every slide reachable.
Affected properties
ring rotation transitionautoplay3D ring (flat list at none)API Reference
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
items | React.ReactNode[] | Yes | - | Slide content. Any node, no image urls required. |
label | string | No | Carousel | Accessible name of the carousel region. |
variant | SurfaceVariant | "default" | No | ambient (glinr) | Surface of each slide frame. |
value / defaultValue / onValueChange | number / number / (index) => void | - | 0 | Controlled or uncontrolled front slide index. |
cardWidth / cardHeight | number | - | 200 / 280 | Slide size in pixels. |
gap | number | No | 24 | Space between neighbouring slides on the ring. |
autoPlay / autoPlayInterval | boolean / number | - | false / 3500 | Rotate on a timer. Needs motion level full. |
showControls | boolean | No | true | Previous, next and (with autoplay) pause buttons. |
getSlideLabel | (index: number, total: number) => string | No | "n of N" | Accessible name of each slide. |
autoPlay | boolean | No | false | Rotate on a timer. Needs motion level full. Pauses on hover and focus, with a visible pause button. |
autoPlayInterval | number | No | 3500 | Milliseconds between automatic steps. |
cardHeight | number | No | 280 | Slide height in pixels. |
cardWidth | number | No | 200 | Slide width in pixels. |
defaultValue | number | No | 0 | Uncontrolled initial front slide index. |
onValueChange | (index: number) => void | No | - | Auto-generated from TypeScript source. |
value | number | No | - | Controlled front slide index. |
Source
Import directly from the package or browse the source on GitHub. Click any file to view it.
Import
import { CylinderCarousel } from "@glinui/ui"Dependencies
@glinui/ui@glinui/motion@phosphor-icons/reactGenerated API Snapshot
Beta
Generated API Snapshot
BetaAuto-extracted from TypeScript source in packages/ui/src/components/cylinder-carousel.tsx. This section is in beta and may lag behind hand-curated docs. Regenerate with pnpm --filter @glinui/docs api:generate.
Generated: 2026-10-08T01:41:21.546Z · Full index: /docs/api-metadata
Primary Props Type
CylinderCarouselProps
CylinderCarouselProps
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
autoPlay | boolean | No | false | Rotate on a timer. Needs motion level full. Pauses on hover and focus, with a visible pause button. |
autoPlayInterval | number | No | 3500 | Milliseconds between automatic steps. |
cardHeight | number | No | 280 | Slide height in pixels. |
cardWidth | number | No | 200 | Slide width in pixels. |
defaultValue | number | No | 0 | Uncontrolled initial front slide index. |
gap | number | No | 24 | Gap between neighbouring slides on the ring, in pixels. |
getSlideLabel | (index: number, total: number) => string | No | - | Name for the slide, receives the 0 based index. Defaults to "n of N". |
items | React.ReactNode[] | Yes | - | Slide content. Any ReactNode, no image urls required. |
label | string | No | "Carousel" | Accessible name of the carousel region. |
onValueChange | (index: number) => void | No | - | - |
showControls | boolean | No | true | Show previous and next buttons. |
value | number | No | - | Controlled front slide index. |
variant | SurfaceVariant | "default" | No | - | Surface of each slide frame. Omit to follow the ambient style. |