Carousels

Thumb Progress Carousel

A carousel component featuring auto-play functionality and a thumbnail navigation strip that visualizes the remaining time for the current slide.

Hero Section Integration

A standard implementation suitable for landing page hero sections. This demo showcases the default 5-second interval, perfect for keeping content moving without overwhelming the user. The layout integrates a text header that stacks gracefully on smaller screens.

Explore the Wilderness

Explore the Wilderness

Mountain Expeditions

Mountain Expeditions

Serene Lakes

Serene Lakes

Coastal Dreams

Coastal Dreams

High-Energy Retail

Demonstrates how adjusting the interval prop to 3000ms creates a faster-paced, higher-energy vibe ideal for flash sales, fashion drops, or promotional content where you want to cycle through products quickly to capture attention.

Flash Sale
Summer Collection

Summer Collection

Urban Streetwear

Urban Streetwear

Exclusive Accessories

Exclusive Accessories

A sophisticated layout for art or architecture portfolios. By increasing the interval to 8000ms, the carousel encourages users to linger on details. This demo also illustrates how the component fits into a side-by-side layout using responsive grid techniques.

Architectural
Digest

Discover the beauty of modern engineering. Our portfolio showcases innovative designs that challenge the conventional boundaries of space and form.

Modern Structures

Modern Structures

Geometric Harmony

Geometric Harmony

Urban Symmetry

Urban Symmetry

Installation

npx shadcn-ui@latest add thumb-progress-carousel

Manual

This component relies on the shadcn/ui Carousel component.

npx shadcn@latest add https://satisui.xyz/r/thumb-progres-carousel.json

Copy the following code into components/thumb-progress-carousel.tsx.

'use client';

import * as React from 'react';
import Image from 'next/image';
import {
  Carousel,
  CarouselApi,
  CarouselContent,
  CarouselItem,
} from '@/components/ui/carousel';
import { cn } from '@/lib/utils';

interface CarouselItemData {
  id: string;
  image: string;
  title: string;
}

interface ThumbProgressCarouselProps {
  /**
   * The array of slides to display.
   */
  items: CarouselItemData[];
  /**
   * Auto-play interval duration in milliseconds.
   * @default 5000
   */
  interval?: number;
}

/**
 * A carousel component featuring auto-play functionality and a thumbnail navigation
 * strip that visualizes the remaining time for the current slide.
 */
export function ThumbProgressCarousel({
  items,
  interval = 5000,
}: ThumbProgressCarouselProps) {
  const [api, setApi] = React.useState<CarouselApi>();
  const [current, setCurrent] = React.useState(0);
  const [progress, setProgress] = React.useState(0);
  const [isPaused, setIsPaused] = React.useState(false);

  React.useEffect(() => {
    if (!api) return;

    const onSelect = () => {
      setCurrent(api.selectedScrollSnap());
      setProgress(0);
    };

    api.on('select', onSelect);

    return () => {
      api.off('select', onSelect);
    };
  }, [api]);

  React.useEffect(() => {
    if (!api || isPaused) return;

    const tickRate = 50;
    const step = 100 / (interval / tickRate);

    const timer = setInterval(() => {
      setProgress((prev) => {
        if (prev >= 100) {
          return 100;
        }
        return prev + step;
      });
    }, tickRate);

    return () => clearInterval(timer);
  }, [api, isPaused, interval]);

  React.useEffect(() => {
    if (api && progress >= 100) {
      api.scrollNext();
    }
  }, [api, progress]);

  const onThumbClick = (index: number) => {
    if (!api) return;
    api.scrollTo(index);
  };

  return (
    <div
      className='relative w-full max-w-5xl mx-auto group overflow-hidden rounded-xl bg-black'
      onMouseEnter={() => setIsPaused(true)}
      onMouseLeave={() => setIsPaused(false)}
    >
      <Carousel setApi={setApi} className='w-full' opts={{ loop: true }}>
        <CarouselContent>
          {items.map((item) => (
            <CarouselItem key={item.id}>
              <div className='relative aspect-[16/9] w-full overflow-hidden'>
                <Image
                  src={item.image}
                  alt={item.title}
                  fill
                  className='object-cover transition-transform duration-700 ease-in-out group-hover:scale-105'
                  priority
                />
                <div className='absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent pointer-events-none' />

                <div className='absolute bottom-32 left-8 md:left-12 max-w-lg pointer-events-none'>
                  <h2 className='text-3xl md:text-5xl font-bold text-white tracking-tight drop-shadow-lg'>
                    {item.title}
                  </h2>
                </div>
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
      </Carousel>

      <div className='absolute bottom-8 left-8 md:left-12 flex gap-3 z-10'>
        {items.map((item, index) => {
          const isActive = current === index;

          return (
            <button
              key={item.id}
              onClick={() => onThumbClick(index)}
              className={cn(
                'relative h-14 w-14 md:h-20 md:w-20 rounded-lg overflow-hidden border-2 transition-all duration-300 ease-out',
                isActive
                  ? 'border-primary border-4 shadow-lg scale-110'
                  : 'border-white/20 opacity-70 hover:opacity-100 hover:scale-105'
              )}
            >
              <Image
                src={item.image}
                alt={`Go to slide ${index + 1}`}
                fill
                className='object-cover'
              />

              {isActive && (
                <div className='absolute inset-0 bg-black/40'>
                  <div
                    className='h-full bg-primary/40 absolute left-0 top-0 transition-all ease-linear'
                    style={{
                      width: `${progress}%`,
                      // GOTCHA: Disable transition when resetting to 0 to prevent the bar from "sliding backward" visibly.
                      transitionDuration: progress === 0 ? '0ms' : '50ms',
                    }}
                  />
                </div>
              )}
            </button>
          );
        })}
      </div>
    </div>
  );
}

Usage

To use the ThumbProgressCarousel, prepare an array of CarouselItemData objects. You can customize the auto-play speed using the interval prop.

import { ThumbProgressCarousel } from '@/components/thumb-progress-carousel';

const slides = [
  {
    id: '1',
    image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba',
    title: 'Majestic Mountains',
  },
  {
    id: '2',
    image: 'https://images.unsplash.com/photo-1682687220063-4742bd7fd538',
    title: 'Urban Exploration',
  },
  {
    id: '3',
    image: 'https://images.unsplash.com/photo-1682687220199-d0124f48f95b',
    title: 'Ocean Breeze',
  },
];

export default function Example() {
  return (
    <div className='p-4 w-full flex justify-center'>
      {/* Default interval (5000ms) */}
      <ThumbProgressCarousel items={slides} />

      {/* Custom fast interval (3000ms) */}
      {/* <ThumbProgressCarousel items={slides} interval={3000} /> */}
    </div>
  );
}

Props

PropTypeDefaultDescription
itemsCarouselItemData[]RequiredThe array of slides to display.
intervalnumber5000Auto-play interval duration in milliseconds.