# Relay Carousel: Conference Speakers

> A speaker lineup with each talk's line in a serif through `quoteClassName`. The schedule under the heading follows the lead through `index` and `onIndexChange`, and picking a time moves the strip the short way round.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/relay-carousel-speakers-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/relay-carousel-speakers-demo
- Screenshot: https://shadcn.exchange/shot/keyframeui/relay-carousel-speakers-demo/light.png
- Registry item: https://keyframeui.com/r/relay-carousel-speakers-demo.json
- Categories: carousel, testimonials, motion
- Listed: 2026-10-27, source last changed 2026-10-27

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/relay-carousel-speakers-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/relay-carousel.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/relay-carousel#relay-carousel-speakers-demo

## Files

### `registry/demos/relay-carousel-speakers-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { RelayCarousel, RelayCarouselItem } from "@/components/ui/relay-carousel";

const talks = [
  {
    name: "Ana Duarte",
    role: "Head of Design, Tinta",
    day: "Thu",
    time: "10:00",
    title: "Designing for the second visit",
    quote:
      "The first visit is a demo. The second one is the product, and that's where most of us stop designing.",
    photo: "1679579664878-2c025e3f87b0",
    alt: "A speaker with a headset microphone gesturing on a pink-lit stage",
  },
  {
    name: "Kwame Asante",
    role: "CTO, Okto",
    day: "Thu",
    time: "11:30",
    title: "Boring infrastructure, on purpose",
    quote:
      "Every clever system I've built has paged me at three in the morning. The boring ones keep running while I sleep.",
    photo: "1731395157985-76af28b68afa",
    alt: "A speaker in a varsity jacket talking into a handheld microphone",
  },
  {
    name: "Mei Lin",
    role: "Research Lead, Northwind",
    day: "Thu",
    time: "14:00",
    title: "What people do when nobody's watching",
    quote:
      "Interviews tell you what people remember. Session notes tell you what they did, and the two rarely agree.",
    photo: "1772419216340-fd8abb4f55de",
    alt: "A speaker with a handheld microphone at a lectern against a dark backdrop",
  },
  {
    name: "Jonas Berg",
    role: "Founder, Field & Fold",
    day: "Thu",
    time: "16:30",
    title: "Pricing a tool for small shops",
    quote:
      "Small shops don't want a cheaper plan. They want a price they can explain to their accountant in one sentence.",
    photo: "1575707751065-42256084fbb7",
    alt: "A speaker in a navy blazer mid-sentence against warm event lights",
  },
  {
    name: "Oliver Grant",
    role: "Principal Engineer, Northwind",
    day: "Fri",
    time: "09:30",
    title: "Accessibility is a performance problem",
    quote:
      "A page that waits on twelve scripts before it answers a screen reader isn't slow for some people. It's broken for them.",
    photo: "1762158007969-eb58e74ee3d3",
    alt: "A speaker in glasses with his arms open wide at a podium",
  },
  {
    name: "Ravi Menon",
    role: "Motion Designer, Tinta",
    day: "Fri",
    time: "11:00",
    title: "Motion with a job to do",
    quote:
      "Animation earns its place when it explains where something went. The rest is a loading spinner in disguise.",
    photo: "1769755409608-e61191208f91",
    alt: "A speaker in a black blazer holding a microphone on a dark stage",
  },
];

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=880&h=840&q=80`;

export function RelayCarouselSpeakersDemo() {
  const [index, setIndex] = React.useState(0);
  const talk = talks[index];

  return (
    <div className="flex min-h-[var(--demo-height,auto)] w-full flex-col justify-center overflow-hidden rounded-2xl border border-stone-200 bg-stone-100 py-14 text-stone-950 sm:py-20 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50">
      <RelayCarousel
        aria-labelledby="relay-speakers-heading"
        index={index}
        onIndexChange={setIndex}
        heading={
          <div>
            <h2
              id="relay-speakers-heading"
              className="text-[2rem] leading-[1.02] font-semibold tracking-[-0.035em] text-balance @2xl:text-5xl"
            >
              On stage at
              <br />
              <span className="text-stone-400 dark:text-stone-500">Northwind Live.</span>
            </h2>
            <p className="mt-5 max-w-sm text-sm leading-6 text-stone-600 dark:text-stone-400">
              <span className="font-medium text-stone-950 tabular-nums dark:text-stone-50">
                {talk.day} {talk.time}
              </span>{" "}
              · {talk.title}
            </p>
            <ol aria-label="Schedule" className="mt-4 flex flex-wrap items-center gap-1.5">
              {talks.map((item, i) => (
                <li key={item.name} className="flex items-center gap-1.5">
                  {item.day !== talks[i - 1]?.day && (
                    <span
                      aria-hidden
                      className={cn(
                        "pr-1 text-xs font-medium text-stone-500 dark:text-stone-400",
                        i > 0 && "pl-2.5",
                      )}
                    >
                      {item.day}
                    </span>
                  )}
                  <button
                    type="button"
                    aria-current={i === index ? "true" : undefined}
                    aria-label={`${item.day} ${item.time}, ${item.name}`}
                    onClick={() => setIndex(i)}
                    className={cn(
                      "h-8 cursor-pointer rounded-full border px-3 text-xs font-medium tabular-nums transition-colors duration-200",
                      "outline-stone-950 focus-visible:outline-2 focus-visible:outline-offset-2 dark:outline-stone-50",
                      i === index
                        ? "border-stone-950 bg-stone-950 text-stone-50 dark:border-stone-50 dark:bg-stone-50 dark:text-stone-950"
                        : "border-stone-300 text-stone-600 hover:border-stone-500 hover:text-stone-950 dark:border-stone-700 dark:text-stone-400 dark:hover:border-stone-500 dark:hover:text-stone-50",
                    )}
                  >
                    {item.time}
                  </button>
                </li>
              ))}
            </ol>
          </div>
        }
      >
        {talks.map((item) => (
          <RelayCarouselItem
            key={item.name}
            name={item.name}
            role={item.role}
            label={`${item.day} ${item.time} · Main stage`}
            quote={item.quote}
            quoteClassName="font-serif font-normal tracking-normal text-[clamp(1.0625rem,calc(var(--relay-l)_*_0.056),1.4375rem)] leading-[1.32]"
            image={photo(item.photo)}
            alt={item.alt}
          />
        ))}
      </RelayCarousel>
    </div>
  );
}
```
