Claude-skill-registry image-carousel
Creates image carousels with hover-activated auto-advance, touch swipe support, and animated progress indicators. Use when building image galleries, product showcases, or any multi-image display with navigation.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/image-carousel" ~/.claude/skills/majiayu000-claude-skill-registry-image-carousel && rm -rf "$T"
manifest:
skills/data/image-carousel/SKILL.mdsource content
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 */} <div className="absolute bottom-3 left-1/2 -translate-x-1/2"> <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) => ( <button key={index} onClick={() => { setCurrentIndex(index); setIsPaused(true); setProgressKey((prev) => prev + 1); setTimeout(() => setIsPaused(false), 3000); }} className="relative cursor-pointer" > {/* Background pill */} <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 && isHovered && !isPaused && ( <div key={progressKey} className="absolute inset-0 h-2 rounded-full bg-white/50 origin-left animate-carousel-progress" style={{ animationDuration: "3000ms" }} /> )} </button> ))} </div> </div> </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
forces animation restart on index changeprogressKey
Indicator Sizing
| Context | Active Width | Inactive Width | Height |
|---|---|---|---|
| Preview (compact) | | | |
| Detail page | | | |
Timing Configuration
| Duration | Use |
|---|---|
| Auto-advance interval |
| Pause duration after manual interaction |
| Image fade transition |
| Indicator pill expansion |
Checklist
-
on container for proper scroll behaviortouch-pan-y - Images use
prop withfillobject-cover -
state for animation restartprogressKey - Pause timeout clears and resumes correctly
-
keyframes added to globals.cssanimate-carousel-progress