Skip to content
Template Empire
Browse components

Accessible Image Carousel

A free React image carousel with keyboard buttons, touch drag and pause-aware autoplay.

Customise

16px
0.12
0.45s
0ms
24px

Code

<MediaImageCarousel
  slides={[
    {
      id: "harbor-light",
      src: "/harbour-light.jpg",
      alt: "Harbour town silhouetted against a warm dusk sky",
      caption: "Harbour Light, Nordvik",
      width: 1600,
      height: 1000,
    },
    {
      id: "glass-ridge",
      src: "/glass-ridge.jpg",
      alt: "Glacial ridgeline reflected in still turquoise water",
      caption: "Glass Ridge, Halden",
      width: 1600,
      height: 1000,
    },
    // ...two to ten slides total
  ]}
  label="Featured destinations"
/>

Accessible Image Carousel is a free React image carousel for Next.js and Tailwind that moves visitors through two to ten images with previous and next buttons, dot indicators, arrow keys or a thresholded touch drag, and announces every manual change to assistive technology. Built with Motion, it ships three transition styles (a sliding peeked track, a crossfade and a bounded scale), so the same slides array can read as three different carousels.

Autoplay is optional and off by default; once configured it starts only after hydration, always shows a Pause/Play button, and pauses on hover, keyboard focus, an active drag, the carousel leaving the viewport, a hidden browser tab or a reduced-motion preference, restarting a fresh full interval after any of those clears. Automatic advances update the live region quietly (aria-live="off") while every deliberate navigation announces the new slide politely. With prefers-reduced-motion enabled, autoplay never starts and every slide change swaps instantly with full keyboard and touch controls.

Install

Add the @te registry to your components.json once:

{
  "registries": {
    "@te": "https://templateempire.io/r/{name}.json"
  }
}

Then install the component:

npx shadcn@latest add @te/media-image-carousel

Usage

import { MediaImageCarousel } from "@/components/media-image-carousel";

const slides = [
  {
    id: "harbor-light",
    src: "/harbour-light.jpg",
    alt: "Harbour town silhouetted against a warm dusk sky",
    caption: "Harbour Light, Nordvik",
    width: 1600,
    height: 1000,
  },
  {
    id: "glass-ridge",
    src: "/glass-ridge.jpg",
    alt: "Glacial ridgeline reflected in still turquoise water",
    caption: "Glass Ridge, Halden",
    width: 1600,
    height: 1000,
  },
  // ...two to ten slides total
];

export function Example() {
  return (
    <MediaImageCarousel
      slides={slides}
      transition="slide"
      autoplayInterval={5000}
      label="Featured destinations"
    />
  );
}

Props

PropTypeDefaultDescription
slides{ id; src; alt; caption?; width; height }[]requiredTwo to ten slides with unique ids and stable dimensions used to reserve the viewport.
initialIdstringundefinedInitial slide id. Falls back to the first slide when omitted or unknown.
transition"slide" | "crossfade" | "scale""slide"Slide-change treatment.
gapnumber16Track gap in CSS pixels for "slide" mode, clamped to 0 to 40.
peeknumber0.12Adjacent-slide fraction shown at each edge in "slide" mode, clamped to 0 to 0.3.
durationnumber0.45Transition duration in seconds, clamped to 0 to 0.9.
autoplayIntervalnumber0Milliseconds between automatic slides. Zero disables autoplay; any other value is clamped to 2500 to 12000.
loopbooleantrueWhether previous and next wrap at the ends.
radiusnumber24Media viewport radius in CSS pixels, clamped to 0 to 44.
labelstring"Image carousel"Accessible name for the carousel region.
onSlideChange(id: string) => voidundefinedCalled with the new slide id after an explicit or automatic slide change.
classNamestringundefinedAdditional classes merged onto the carousel region.

Customisation

Three Tier 3 tokens retheme the carousel without touching code. --image-carousel-accent colours the active dot and the control focus ring and defaults to var(--primary); --image-carousel-panel fills the stage and controls, chaining var(--card, var(--surface)); --image-carousel-divider borders the controls and defaults to var(--border). Geometry and pacing come from the gap, peek, duration and radius props:

<MediaImageCarousel
  slides={slides}
  transition="scale"
  radius={12}
  className="[--image-carousel-accent:var(--primary-accent)]"
/>

Frequently asked questions

Is Accessible Image Carousel free for commercial use?
Yes. Accessible Image Carousel is MIT licensed, so you can use it in personal and commercial projects without attribution.
Is the carousel accessible and safe under reduced motion?
Yes. The region carries the APG carousel role, each slide is a named group with required alt text and an optional figcaption, and only the active slide is exposed and focusable. Previous, next, every dot and the optional Pause/Play control are native buttons with visible focus. Touch dragging respects vertical scrolling and never traps focus. With prefers-reduced-motion enabled, autoplay never starts and slides swap instantly while every control keeps working.
How do I change the transition style and autoplay pace?
Set transition to slide, crossfade or scale to change how slides change without touching the slides array, and pass autoplayInterval in milliseconds (clamped to 2500 to 12000) to enable a pause-aware autoplay timer with its own visible control. Leave autoplayInterval unset for a fully manual carousel.

Related components

Want the full page this belongs to?Template Empire builds complete Next.js templates with this motion system throughout. Browse the paid templates that pair with these free components.Explore Template Empire templates