Carousel
One poster at a time, each name set so large the frame crops it. The count between the arrows rolls the way you travel.
Report an issue with CarouselExample
01 / 05
01 / 05
01 / 05
poster, first slide
01 / 03
line, first slide
01 / 03
shelf, first slide
01 / 03
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/carousel.jsonUsage
This example also uses Meta.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ meta.jsonimport { Carousel, CarouselItem, CarouselTitle } from "@/components/ui/carousel"import { Meta } from "@/components/ui/meta"const work = [["Halden", "Identity", "2026"],["Northlight", "Website", "2026"],["Oda Studio", "Identity", "2025"],["Tidewater", "Motion", "2025"],["Marram", "Website", "2025"],]const slides = work.map(([name, kind, year]) => (<CarouselItem key={name}><CarouselTitle>{name}</CarouselTitle><Meta><span>{kind}</span><span>{year}</span></Meta></CarouselItem>))export default function Example() {return (<div className="grid gap-y-24"><Carousel aria-label="Selected work" trackLabel="Projects" previousLabel="Previous project" nextLabel="Next project">{slides}</Carousel><Carousel variant="line" aria-label="Selected work, as a line" trackLabel="Projects" previousLabel="Previous project" nextLabel="Next project">{slides}</Carousel><Carousel variant="shelf" aria-label="Selected work, on the shelf" trackLabel="Projects" previousLabel="Previous project" nextLabel="Next project">{slides}</Carousel></div>)}const three = work.slice(0, 3).map(([name, kind]) => (<CarouselItem key={name}><CarouselTitle>{name}</CarouselTitle><Meta><span>{kind}</span></Meta></CarouselItem>))Contract
- Underneath: scroll snap, plus a hook.
- Anatomy:
<div class="ot-carousel" data-variant="poster | line | shelf" role="region" aria-roledescription="carousel">holding.ot-carousel-track(scroll-snap,tabindex="0") of.ot-carousel-slide(role="group", "n of m",data-currenton the one you're on) with.ot-carousel-title, then.ot-carousel-navwith ← and → buttons around.ot-carousel-count(.ot-carousel-now). On the shelf each closed slide also holds.ot-carousel-spine, a button over it. - One poster at a time, each name set so large it is cropped: it sinks under a hairline that cuts off its feet, the way SPECTRA's rule crops its word, and a name wider than the frame runs off its end. The count rolls the way you travel; the slide whose start edge is nearest the track's start is the one you're on, read on scroll, so it stays true while you swipe.
- Line: every name runs on as one line of display type, each closed by a full stop, and you read along it. The name at the start is the one you're on: it inks, its full stop takes the accent and its facts appear under it; the rest wait in pencil, the next one cropped by the frame's end. A spacer after the last name lets it, too, come to the start, the end of the line. Forced colours set the waiting names in GrayText.
- Shelf: the slides stand as spines, a hairline between each, their names running up them in the pencil like books on a shelf (the sheet's spine, made a whole shelf). The one you take out widens and turns to face you, its name sinking under its hairline and cropped by the next spine; the one you put back narrows into a spine. The track doesn't scroll. Pointing at a spine inks its name; clicking takes it out.
- Keyboard: Left and Right on the track, Home and End, or the buttons. Keys in inputs, textareas, selects and editable text stay with those controls; a key already handled by a slide stays handled. The spines are pointer targets only (
tabindex="-1"): the keyboard has the track and the buttons.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "poster" | "line" | "shelf" | "poster" | poster: one slide at a time, the name sinking under a hairline that cuts off its feet. line: every name runs on as one line of display type, each closed by a full stop; the name at the start is the one you're on, inked, its full stop in the accent and its facts shown under it, while the next waits in pencil, cropped by the frame. shelf: the slides stand as spines with a hairline between them, each name running up its spine; the one you take out widens and turns to face you, and the name is cropped by the next spine. Click a spine to take it out. |
CarouselItem | div | A slide. Direct children of Carousel only: each is labelled "n of m" and announced as a slide. | |
trackLabel | string | "Slides" | The track takes focus so the arrow keys (and Home and End) can move it, so it needs a name. Say what the slides are. |
previousLabel / nextLabel | string | "Previous" / "Next" | Names for the arrow buttons. They disable at the ends. |
CarouselTitle | p | A name set so large the slide's edge crops it. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Carousel | POINT / SPECTRA | One poster at a time, the name sunk under a hairline |
| Line carousel | "It has to be design." (the accent full stop); SPECTRA crop | The names as one line of type, read along |
| Shelf carousel | Books on a shelf | Spines with the names running up them; one taken out faces you |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Carousel | Roll | The count rolls the way you travel |
| Line carousel | Reading on | The line slides; the name arriving at the start inks, its full stop takes the accent and its facts appear, while the one you left goes back to pencil |
| Shelf carousel | Taken out | The spine widens and its name turns to face you; the other narrows back into a spine |