IX Surfaces

Star on GitHubFeedback
IX SurfacesNative HTML and a hookot-carousel

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 Carousel

Example

poster, first slide
line, first slide
shelf, first slide

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

Usage

This example also uses Meta.

npx shadcn@latest add https://thedirectors.agency/ui/r/meta.json
carousel-example.tsx
import { 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-current on the one you're on) with .ot-carousel-title, then .ot-carousel-nav with ← 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

Props of Carousel
PropTypeDefaultWhat 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.
CarouselItemdivA slide. Direct children of Carousel only: each is labelled "n of m" and announced as a slide.
trackLabelstring"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 / nextLabelstring"Previous" / "Next"Names for the arrow buttons. They disable at the ends.
CarouselTitlepA name set so large the slide's edge crops it.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
CarouselPOINT / SPECTRAOne poster at a time, the name sunk under a hairline
Line carousel"It has to be design." (the accent full stop); SPECTRA cropThe names as one line of type, read along
Shelf carouselBooks on a shelfSpines with the names running up them; one taken out faces you

How it moves

How it moves
ComponentArticulationWhat moves
CarouselRollThe count rolls the way you travel
Line carouselReading onThe 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 carouselTaken outThe spine widens and its name turns to face you; the other narrows back into a spine