Glin UI
Primitive / MoleculeAdapted from Vengeance UIbeta

Cylinder Carousel

Slides on a rotating 3D ring with buttons, arrow keys, swipe and a pausable autoplay.

Edit on GitHub

Installation

Install from
pnpm dlx @glinui/cli@latest add cylinder-carousel

Credits. 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.

glinr
AuroraSlide 1
VoltageSlide 2
PaletteSlide 3
PrismSlide 4
SignalSlide 5
plain
AuroraSlide 1
VoltageSlide 2
PaletteSlide 3
PrismSlide 4
SignalSlide 5
solid
AuroraSlide 1
VoltageSlide 2
PaletteSlide 3
PrismSlide 4
SignalSlide 5
<CylinderCarousel variant="plain" items={items} cardWidth={140} cardHeight={180} />

States

Autoplay pauses on hover and focus, and has a pause button.

AuroraSlide 1
VoltageSlide 2
PaletteSlide 3
PrismSlide 4
SignalSlide 5
<CylinderCarousel autoPlay autoPlayInterval={2800} items={items} />

In a layout

A product section with copy beside the ring.

New

Pick a palette

Spin through curated palettes with the arrows, swipe or the arrow keys.

AuroraSlide 1
VoltageSlide 2
PaletteSlide 3
PrismSlide 4
SignalSlide 5
VaultSlide 6
<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

KeyAction
ArrowRight / ArrowLeftNext or previous slide (reversed in RTL).
Home / EndFirst or last slide.
TabMoves 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

PropTypeRequiredDefaultDescription
itemsReact.ReactNode[]Yes-Slide content. Any node, no image urls required.
labelstringNoCarouselAccessible name of the carousel region.
variantSurfaceVariant | "default"Noambient (glinr)Surface of each slide frame.
value / defaultValue / onValueChangenumber / number / (index) => void-0Controlled or uncontrolled front slide index.
cardWidth / cardHeightnumber-200 / 280Slide size in pixels.
gapnumberNo24Space between neighbouring slides on the ring.
autoPlay / autoPlayIntervalboolean / number-false / 3500Rotate on a timer. Needs motion level full.
showControlsbooleanNotruePrevious, next and (with autoplay) pause buttons.
getSlideLabel(index: number, total: number) => stringNo"n of N"Accessible name of each slide.
autoPlaybooleanNofalseRotate on a timer. Needs motion level full. Pauses on hover and focus, with a visible pause button.
autoPlayIntervalnumberNo3500Milliseconds between automatic steps.
cardHeightnumberNo280Slide height in pixels.
cardWidthnumberNo200Slide width in pixels.
defaultValuenumberNo0Uncontrolled initial front slide index.
onValueChange(index: number) => voidNo-Auto-generated from TypeScript source.
valuenumberNo-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/react

Generated API Snapshot

Beta

Auto-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

PropTypeRequiredDefaultDescription
autoPlaybooleanNofalseRotate on a timer. Needs motion level full. Pauses on hover and focus, with a visible pause button.
autoPlayIntervalnumberNo3500Milliseconds between automatic steps.
cardHeightnumberNo280Slide height in pixels.
cardWidthnumberNo200Slide width in pixels.
defaultValuenumberNo0Uncontrolled initial front slide index.
gapnumberNo24Gap between neighbouring slides on the ring, in pixels.
getSlideLabel(index: number, total: number) => stringNo-Name for the slide, receives the 0 based index. Defaults to "n of N".
itemsReact.ReactNode[]Yes-Slide content. Any ReactNode, no image urls required.
labelstringNo"Carousel"Accessible name of the carousel region.
onValueChange(index: number) => voidNo--
showControlsbooleanNotrueShow previous and next buttons.
valuenumberNo-Controlled front slide index.
variantSurfaceVariant | "default"No-Surface of each slide frame. Omit to follow the ambient style.