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 */}
<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
progressKeyforces 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-yon container for proper scroll behavior - Images use
fillprop withobject-cover -
progressKeystate for animation restart - Pause timeout clears and resumes correctly
-
animate-carousel-progresskeyframes added to globals.css
More from ainergiz/design-inspirations
glassmorphism
Creates frosted glass UI elements with blur, transparency, and subtle borders. Use when building overlays, floating controls, tooltips, or any element that should appear elevated with a translucent background.
170stacked-cards
Creates horizontally fanned/cascading card stacks with proper z-index ordering and hover lift animations. Use when building album browsers, card fans, stacked previews, or any overlapping card collection.
5nested-card
Creates cards with an outer gradient container and inner content card. Use when building premium card designs with depth, layered card layouts, or cards with image sections and content sections.
5expandable-card
Creates expandable/collapsible cards using CSS grid-rows animation with smooth transitions. Use when building accordions, expandable panels, collapsible sections, or show/hide card content.
5create-new-design
Scaffolds a new design in the design-inspirations repo with preview component, detail page, and main page entry. Use when creating a new design, adding a design inspiration, or scaffolding design files.
5dropdown-menu
Creates dropdown menus with proper click-outside detection and z-index stacking for list contexts. Use when building action menus, context menus, or any dropdown that appears in cards/list items.
4