Relay Carousel

Testimonial Carousel for React

A looping strip of photo cards where the first card leads: bigger, with its quote showing. Passing the lead on glides the strip, shrinks the old lead out of the way and grows the next card into its place, then sets the new quote in line by line.

  • Carousel
  • Testimonials
  • Motion

Uses motion, clsx, tailwind-merge

Inspired by Glide

Open in ChatGPTOpen in Claude

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/relay-carousel.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/relay-carousel

Examples

Finished sections built with Relay Carousel. Each one installs whole, with the component included.

Props

children Required

TypeReact.ReactNode

The cards, as RelayCarouselItems. The strip loops, and repeats them when there are too few to fill a wide screen.

heading

TypeReact.ReactNode

Shown top left, lined up with the lead card. The controls sit opposite it, and move under the strip on narrow screens.

autoPlay

Typenumber

Passes the lead on every this many ms. Adds a pause button whose ring fills as the timer. Holds while the pointer or keyboard focus is inside or the carousel is off screen. Off for reduced motion until the visitor presses play.

defaultIndex

TypenumberDefault0

The card that leads first.

index

Typenumber

The card that leads. Set it with onIndexChange to control the carousel from outside; it moves the short way round.

onIndexChange

Type(index: number) => void

Called with the new lead's index after a button, a click on a card, a drag, a swipe, a key or autoplay moves it.

inset

TypestringDefault"max(2.5rem, calc((100cqi - 76rem) / 2))"

Left edge of the heading and the lead card, as a CSS length (1.25rem minimum on narrow screens). cqi is the carousel's width, so the default lines up with a 76rem page column. Cards that already led fade out in this margin.

duration

TypenumberDefault750

Milliseconds the strip takes to glide to a new lead, on a spring with no bounce. A flick's speed carries into it.

stagger

TypenumberDefault80

Milliseconds between the lead's quote lines setting in. Content in children follows the last line.

drift

TypenumberDefault14

How far each photo drifts inside its card per card the strip travels, in px. 0 keeps the photos still. Off for reduced motion.

className

Typestring

Classes for the section. The controls are drawn in its text color.

item.name Required

Typestring

RelayCarouselItem: who said it. Shown on every card.

item.role

TypeReact.ReactNode

RelayCarouselItem: their role and company, under the name.

item.quote Required

Typestring

RelayCarouselItem: what they said, without quotation marks. Shown while the card leads; around 130 characters fits a phone.

item.image Required

Typestring

RelayCarouselItem: photo URL. It's cut to the lead card's size and anchored at the top, so a resting card shows its top middle.

item.alt

TypestringDefault""

RelayCarouselItem: alt text for the photo.

item.label

TypeReact.ReactNode

RelayCarouselItem: small text in the lead card's top corner, like a place or a product.

item.children

TypeReact.ReactNode

RelayCarouselItem: pinned to the lead card's bottom right corner once its quote is in, like a link to the full story.

item.quoteClassName

Typestring

RelayCarouselItem: classes for the quote, like a serif face or a size.

item.imageClassName

Typestring

RelayCarouselItem: classes for the photo. The card is a group/relay with data-state="lead" or "rest".

item.className

Typestring

RelayCarouselItem: classes for the card, like its corner radius or background.