image-carousel

Image Carousel Pattern

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "image-carousel" with this command: npx skills add ainergiz/design-inspirations/ainergiz-design-inspirations-image-carousel

Image Carousel Pattern

Build smooth image carousels that auto-advance on hover with touch swipe support and animated progress indicators.

Core Features

  • Hover-activated: Auto-advance starts only when user hovers (not on page load)

  • Touch swipe: Mobile-friendly swipe navigation with threshold detection

  • Progress indicators: Glassmorphic pill indicators with animated fill

  • Pause on interaction: Manual navigation pauses auto-advance temporarily

State Management

const [currentIndex, setCurrentIndex] = useState(0); const [isHovered, setIsHovered] = useState(false); const [progressKey, setProgressKey] = useState(0); // Forces animation restart const [isPaused, setIsPaused] = useState(false); const [touchStart, setTouchStart] = useState<number | null>(null);

Core Implementation

"use client";

import { useState, useEffect } from "react"; import Image from "next/image";

const images = [ "/images/image-1.jpeg", "/images/image-2.jpeg", "/images/image-3.jpeg", ];

function ImageCarousel() { const [currentIndex, setCurrentIndex] = useState(0); const [isHovered, setIsHovered] = useState(false); const [progressKey, setProgressKey] = useState(0); const [isPaused, setIsPaused] = useState(false); const [touchStart, setTouchStart] = useState<number | null>(null);

// Auto-advance effect - only when hovered and not paused useEffect(() => { if (!isHovered || isPaused) return;

const interval = setInterval(() => {
  setCurrentIndex((prev) => (prev + 1) % images.length);
  setProgressKey((prev) => prev + 1);
}, 3000);
return () => clearInterval(interval);

}, [isHovered, isPaused]);

// Touch handlers const handleTouchStart = (e: React.TouchEvent) => { setTouchStart(e.touches[0].clientX); };

const handleTouchEnd = (e: React.TouchEvent) => { if (touchStart === null) return;

const touchEnd = e.changedTouches[0].clientX;
const diff = touchStart - touchEnd;
const threshold = 50;

if (Math.abs(diff) > threshold) {
  if (diff > 0) {
    // Swipe left - next image
    setCurrentIndex((prev) => (prev + 1) % images.length);
  } else {
    // Swipe right - previous image
    setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
  }
  setProgressKey((prev) => prev + 1);
  setIsPaused(true);
  setTimeout(() => setIsPaused(false), 3000);
}
setTouchStart(null);

};

return ( <div className="relative h-full w-full group touch-pan-y" onMouseEnter={() => { setIsHovered(true); setProgressKey((prev) => prev + 1); }} onMouseLeave={() => setIsHovered(false)} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} > {/* Images with fade transition */} {images.map((src, index) => ( <Image key={src} src={src} alt={Image ${index + 1}} fill className={object-cover transition-opacity duration-700 ease-in-out ${ index === currentIndex ? "opacity-100" : "opacity-0" }} /> ))}

  {/* Glassmorphic indicator container */}
  &#x3C;div className="absolute bottom-3 left-1/2 -translate-x-1/2">
    &#x3C;div className="flex items-center gap-2 px-3 py-2 rounded-full bg-black/20 backdrop-blur-md border border-white/10">
      {images.map((_, index) => (
        &#x3C;button
          key={index}
          onClick={() => {
            setCurrentIndex(index);
            setIsPaused(true);
            setProgressKey((prev) => prev + 1);
            setTimeout(() => setIsPaused(false), 3000);
          }}
          className="relative cursor-pointer"
        >
          {/* Background pill */}
          &#x3C;div
            className={`h-2 rounded-full transition-all duration-300 ${
              index === currentIndex
                ? "w-6 bg-white"
                : "w-2 bg-white/40 hover:bg-white/60"
            }`}
          />
          {/* Animated progress fill - only when hovered and not paused */}
          {index === currentIndex &#x26;&#x26; isHovered &#x26;&#x26; !isPaused &#x26;&#x26; (
            &#x3C;div
              key={progressKey}
              className="absolute inset-0 h-2 rounded-full bg-white/50 origin-left animate-carousel-progress"
              style={{ animationDuration: "3000ms" }}
            />
          )}
        &#x3C;/button>
      ))}
    &#x3C;/div>
  &#x3C;/div>
&#x3C;/div>

); }

Required CSS (add to globals.css)

/* Carousel progress animation */ @keyframes carousel-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.animate-carousel-progress { animation: carousel-progress linear forwards; }

Key Behaviors

Auto-Advance Logic

State Behavior

Not hovered No auto-advance

Hovered + not paused Auto-advance every 3s

Hovered + paused No auto-advance (resumes after 3s)

Touch Swipe

  • Threshold: 50px minimum swipe distance

  • Left swipe: Next image

  • Right swipe: Previous image

  • After swipe: Pause auto-advance for 3s

Progress Indicator

  • Expands from dot (w-2) to pill (w-6) when active

  • Shows animated fill overlay only when hovering and not paused

  • progressKey forces animation restart on index change

Indicator Sizing

Context Active Width Inactive Width Height

Preview (compact) w-4

w-1.5

h-1.5

Detail page w-6

w-2

h-2

Timing Configuration

Duration Use

3000ms

Auto-advance interval

3000ms

Pause duration after manual interaction

700ms

Image fade transition

300ms

Indicator pill expansion

Checklist

  • touch-pan-y on container for proper scroll behavior

  • Images use fill prop with object-cover

  • progressKey state for animation restart

  • Pause timeout clears and resumes correctly

  • animate-carousel-progress keyframes added to globals.css

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

glassmorphism

No summary provided by upstream source.

Repository SourceNeeds Review
General

expandable-card

No summary provided by upstream source.

Repository SourceNeeds Review
General

stacked-cards

No summary provided by upstream source.

Repository SourceNeeds Review