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.
registry/demos/relay-carousel-speakers-demo.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>
);
}
Live preview, examples and props: https://keyframeui.com/components/relay-carousel#relay-carousel-speakers-demo