Componentmedia1.21 KB gzipZero dependencies

Image Carousel

Image carousel with arrows, dots and swipe feel.

Preview

Live Preview

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add image-carousel

Or install the full npm package:

npm install bigbullui
import { ImageCarousel } from "@/components/ui/image-carousel";

<ImageCarousel />

Usage guidance

Use Image Carousel when a ticket stub surface needs Image carousel with arrows, dots and swipe feel. Keep props minimal and prefer composition with sibling stubs.

Avoid Image Carousel for long form flows or dense data grids where a dedicated layout block fits better.

Source code

image-carousel.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface ImageCarouselProps extends React.HTMLAttributes<HTMLDivElement> {
  images: { id: string; src: string; alt?: string }[];
  autoPlay?: boolean;
  interval?: number;
}

/** Image carousel slider: nav arrows + dot pagination + autoplay. */
export function ImageCarousel({ images, autoPlay, interval = 4000, className, ...props }: ImageCarouselProps) {
  const [index, setIndex] = React.useState(0);

  React.useEffect(() => {
    if (!autoPlay || images.length < 2) return;
    const t = setInterval(() => setIndex((i) => (i + 1) % images.length), interval);
    return () => clearInterval(t);
  }, [autoPlay, interval, images.length]);

  const go = (dir: 1 | -1) => setIndex((i) => (i + dir + images.length) % images.length);

  return (
    <div className={cn("relative w-full overflow-hidden rounded-lg border border-border", className)} {...props}>
      <style>{`@keyframes icSlide { from { opacity: 0; transform: scale(1.04); } to { opacity: 1; transform: scale(1); } }`}</style>
      <div className="aspect-video w-full">
        {images.map((img, i) => (
          // eslint-disable-next-line @next/next/no-img-element
          <img
            key={img.id}
            src={img.src}
            alt={img.alt ?? ""}
            className={cn(
              "absolute inset-0 size-full object-cover transition-opacity duration-400 motion-reduce:transition-none",
              i === index ? "opacity-100 animate-[icSlide_0.4s_ease-out] motion-reduce:animate-none" : "opacity-0"
            )}
            loading="lazy"
          />
        ))}
      </div>
      <button
        onClick={() => go(-1)}
        className="absolute left-2 top-1/2 -translate-y-1/2 rounded-full border border-white/30 bg-black/40 p-1.5 text-white transition-colors hover:bg-black/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white motion-reduce:transition-none"
        aria-label="Previous slide"
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
      </button>
      <button
        onClick={() => go(1)}
        className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full border border-white/30 bg-black/40 p-1.5 text-white transition-colors hover:bg-black/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white motion-reduce:transition-none"
        aria-label="Next slide"
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6" /></svg>
      </button>
      <div className="absolute inset-x-0 bottom-2 flex justify-center gap-1.5">
        {images.map((img, i) => (
          <button
            key={img.id}
            onClick={() => setIndex(i)}
            aria-label={`Go to slide ${i + 1}`}
            className={cn("rounded-full transition-all duration-200 motion-reduce:transition-none", i === index ? "h-1.5 w-4 bg-white" : "size-1.5 bg-white/50 hover:bg-white/80")}
          />
        ))}
      </div>
    </div>
  );
}

Props

PropTypeDescription
images{ id: string; src: string; alt?: string }[]
autoPlayboolean
intervalnumber