Skip to content
txbrendan/ui

Slideshow

beta

Photos that crossfade one to the next on their own, each with a caption and a bar that fills while it shows.

Preview

Texture

default

New

Ridges at sunset

West valley · 4:05 · Yesterday

wide

Lake at first light

North shore · 2:41 · 3h ago

one photo

Night over the valley

Ridge camp · 1:12 · Today

no photos

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/slideshow

Use it for

  • A few photos that show what something looks like, like the walks, places, or rooms behind a feature, in a hero or beside a list.
  • Three to six photos. Give each an alt that says what is in it, and a short title and detail for the caption.
  • Pass picture for your own element instead of src, like a next/image.
  • Use photos that fill the frame, with no clear parts. A new photo fades in over the one before, which shows through anything clear until the fade ends.
  • Set a width. It keeps 4:5. Change the shape with className, like "aspect-video".
  • Keep something beside it in step with onIndexChange.
  • Change the time each photo shows with className="[--slideshow-duration:9s]". The default is 6s.
  • Put a small Badge on the top corner with badge.

Avoid it for

  • Photos people must compare or study. Use Media Grid, and Lightbox to view one large.
  • Words or steps people must read in order. Nobody reads a slide that moves on by itself.
  • Two slideshows on one screen. One moving thing is enough.

Props

  • slides

    Required. The photos, in order. It loops back to the first.

    Slide[]

  • Slide

    Type. src is the photo. picture draws your own element in its place. alt says what is in it. title and detail make the caption.

    { src?: string; picture?: ReactNode; alt: string; title?: string; detail?: string; id?: string | number }

  • badge

    Something small on the top corner, like a Badge.

    ReactNode

  • onIndexChange

    Called with the new photo's index each time it changes.

    (index: number) => void

  • texture

    dither draws the photos as dots, gradient fades them from the top, and grain speckles them. Hover fills gradient and grain in. Left out, it follows the Texture around it.

    "flat" | "dither" | "gradient" | "grain"

  • aria-label

    Names the slideshow for screen readers.

    string · default "Slideshow"

  • className

    Extra classes on the root. Set the width, the shape, or --slideshow-duration here. Later classes win.

    string

Accessibility

  • The root is a carousel with a name. Each photo is a slide named like "2 of 4", and only the one showing reaches screen readers and Tab.
  • It holds still while the pointer is over it or keyboard focus is inside it.
  • A pause button stops it for good, on touch screens too. It says it is pressed with aria-pressed. While paused, a new caption is read aloud.
  • Each bar is a button, like "Slide 3 of 4", so every photo can be reached without waiting. The one showing has aria-current.
  • With prefers-reduced-motion, or data-motion="off", it stands still on the first photo, and the bars still change it. The pause button goes away then.
  • The caption and controls sit on a dark veil in both themes, so they stay clear on light photos.

Examples

Photos with captions

<Slideshow
  className="max-w-sm"
  aria-label="Recent photos"
  slides={[
    { src: "/photos/columns.jpg", alt: "Stone columns at dusk", title: "Columns at dusk", detail: "East gallery · Yesterday" },
    { src: "/photos/lake.jpg", alt: "A still lake at sunrise", title: "Lake at first light", detail: "North shore · 3h ago" },
    { src: "/photos/arcade.jpg", alt: "Three arches in a stone wall", title: "Arcade at noon", detail: "Main square · Today" },
  ]}
/>

A list beside it that follows the photo, with a badge and slower slides

const [index, setIndex] = useState(0);

<div className="flex gap-4">
  <Slideshow
    slides={slides}
    badge={<Badge emphasis="soft" size="sm">New</Badge>}
    onIndexChange={setIndex}
    className="max-w-xs [--slideshow-duration:9s]"
  />
  <ul>
    {slides.map((slide, i) => (
      <li key={slide.src} aria-current={i === index ? "true" : undefined}>{slide.title}</li>
    ))}
  </ul>
</div>

Photos drawn as dots

<Slideshow slides={slides} texture="dither" className="max-w-sm" />

Grain photos that clear on hover

<Slideshow slides={slides} texture="grain" className="max-w-sm" />