Browse components
Coverflow Gallery
3D coverflow carousel with keyboard, dot nav, and optional autoplay that pauses on hover.
Add to your project
Pick the way you like to build.
Let your AI assistant add it
Copy this prompt and paste it into the AI tool building your site.
# Integrate this UI component
Add this component to my project.
1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.
## Component source
```tsx
'use client';
import {
useCallback,
useEffect,
useRef,
useState,
type CSSProperties,
type KeyboardEvent,
} from 'react';
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// items Gallery entries — image src, alt, and optional caption
// cardWidth Card width in px
// cardHeight Card height in px
// cornerRadius Border radius in px
// autoplay Auto-advance slides (pauses on hover/focus)
// showCaption Show caption on the focused card
// className Styles for the outer shell
//
type GalleryItem = {
image: { src: string; alt?: string };
caption?: string;
};
type CoverflowGalleryProps = {
items?: GalleryItem[];
cardWidth?: number;
cardHeight?: number;
cornerRadius?: number;
autoplay?: boolean;
showCaption?: boolean;
className?: string;
};
// ---------------------------------------------------------------------------
// Defaults
// ---------------------------------------------------------------------------
const DEFAULT_ITEMS: GalleryItem[] = [
{
image: {
src: 'https://assets.uniquel.io/components/stock/photo-1497366216548-37526070297c.jpg',
alt: 'Minimal office workspace',
},
caption: 'Studio\nLight\nCalm',
},
{
image: {
src: 'https://assets.uniquel.io/components/stock/photo-1487958449943-2429e8be8625.jpg',
alt: 'Modern concrete building',
},
caption: 'Form\nConcrete\nLine',
},
{
image: {
src: 'https://assets.uniquel.io/components/stock/photo-1503387762-592deb58ef4e.jpg',
alt: 'Architectural interior detail',
},
caption: 'Space\nSteel\nFocus',
},
{
image: {
src: 'https://assets.uniquel.io/components/stock/photo-1600607687939-ce8a6c25118c.jpg',
alt: 'Bright living room interior',
},
caption: 'Home\nWarm\nOpen',
},
{
image: {
src: 'https://assets.uniquel.io/components/stock/photo-1486325212027-8081e485255e.jpg',
alt: 'Curved glass tower',
},
caption: 'Curve\nMotion\nDepth',
},
];
const PERSPECTIVE = 1600;
const SCALE_STEP = 0.16;
const MAX_VISIBLE = 2;
const DEPTH = 240;
const MOVE_MS = 600;
const AUTOPLAY_MS = 2500;
const INACTIVE_DIM = 0.4;
const YAW_TILT = 12;
const ROLL_TILT = 8;
const SPREAD = 8;
export default function CoverflowGallery({
items = DEFAULT_ITEMS,
cardWidth = 400,
cardHeight = 400,
cornerRadius = 12,
autoplay = false,
showCaption = true,
className = '',
}: CoverflowGalleryProps) {
const deck = items.length ? items : DEFAULT_ITEMS;
const total = deck.length;
const [focusedIndex, setFocusedIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const inputLockedRef = useRef(false);
const releaseLockLater = useCallback(() => {
inputLockedRef.current = true;
window.setTimeout(() => {
inputLockedRef.current = false;
}, Math.max(50, MOVE_MS));
}, []);
const advance = useCallback(
(direction: number) => {
if (inputLockedRef.current || total < 2) return;
releaseLockLater();
setFocusedIndex((current) => (((current + direction) % total) + total) % total);
},
[releaseLockLater, total]
);
const jumpToIndex = useCallback(
(index: number) => {
if (inputLockedRef.current) return;
releaseLockLater();
setFocusedIndex(index);
},
[releaseLockLater]
);
const focusCard = useCallback(
(index: number) => {
if (inputLockedRef.current || autoplay) return;
releaseLockLater();
setFocusedIndex((current) =>
index === current ? (current + 1) % total : index
);
},
[autoplay, releaseLockLater, total]
);
useEffect(() => {
setFocusedIndex((current) => Math.max(0, Math.min(total - 1, current)));
}, [total]);
useEffect(() => {
if (!autoplay || isPaused || total < 2) return;
const id = window.setInterval(() => advance(1), AUTOPLAY_MS);
return () => window.clearInterval(id);
}, [advance, autoplay, isPaused, total]);
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowRight') {
event.preventDefault();
advance(1);
} else if (event.key === 'ArrowLeft') {
event.preventDefault();
advance(-1);
}
},
[advance]
);
const transitionCss = `transform ${MOVE_MS}ms cubic-bezier(0.22, 1, 0.36, 1), opacity ${MOVE_MS}ms cubic-bezier(0.22, 1, 0.36, 1)`;
const rootStyle: CSSProperties = {
position: 'relative',
width: '100%',
minWidth: 320,
minHeight: 360,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
perspective: `${PERSPECTIVE}px`,
overflow: 'hidden',
outline: 'none',
};
return (
<div
className={['w-full py-8', className].filter(Boolean).join(' ')}
tabIndex={0}
role='group'
aria-roledescription='carousel'
aria-label='Image gallery'
onKeyDown={onKeyDown}
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
onFocus={() => setIsPaused(true)}
onBlur={() => setIsPaused(false)}
>
<div style={rootStyle}>
<div
style={{
position: 'relative',
width: cardWidth,
height: cardHeight,
transformStyle: 'preserve-3d',
}}
>
{deck.map((entry, index) => {
let offset = index - focusedIndex;
if (offset > total / 2) offset -= total;
if (offset < -total / 2) offset += total;
const distance = Math.abs(offset);
const isVisible = distance <= MAX_VISIBLE;
const isFocused = offset === 0;
const scale = Math.max(0.4, 1 - distance * SCALE_STEP);
const translateX = offset * SPREAD * 30;
const translateZ = -distance * DEPTH;
const rotateY = -offset * YAW_TILT;
const rotateZ = offset * ROLL_TILT;
const cardStyle: CSSProperties = {
position: 'absolute',
left: '50%',
top: '50%',
width: cardWidth,
height: cardHeight,
borderRadius: cornerRadius,
overflow: 'hidden',
transformStyle: 'preserve-3d',
transformOrigin: 'center center',
transform: `translate(-50%, -50%) translateX(${translateX}px) translateZ(${translateZ}px) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg) scale(${scale})`,
transition: transitionCss,
opacity: isVisible ? 1 : 0,
cursor: autoplay || isFocused ? 'default' : 'pointer',
pointerEvents: isVisible && !autoplay ? 'auto' : 'none',
backgroundColor: '#171717',
};
return (
<div
key={`${entry.image.src}-${index}`}
style={cardStyle}
onClick={() => focusCard(index)}
aria-label={entry.caption || entry.image.alt}
aria-hidden={!isVisible}
>
<img
src={entry.image.src}
alt={entry.image.alt || entry.caption || ''}
draggable={false}
style={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block',
userSelect: 'none',
}}
/>
{showCaption && entry.caption && isFocused && (
<>
<div
style={{
position: 'absolute',
inset: 0,
background:
'linear-gradient(180deg, rgba(0,0,0,0) 35%, rgba(0,0,0,0.72) 100%)',
pointerEvents: 'none',
}}
/>
<div className='pointer-events-none absolute bottom-0 left-0 p-6 text-white'>
<span className='whitespace-pre-line text-2xl font-bold leading-tight tracking-tight drop-shadow-md'>
{entry.caption}
</span>
</div>
</>
)}
<div
style={{
position: 'absolute',
inset: 0,
background: '#000000',
opacity: isFocused ? 0 : INACTIVE_DIM,
transition: `opacity ${MOVE_MS}ms cubic-bezier(0.22, 1, 0.36, 1)`,
pointerEvents: 'none',
}}
/>
</div>
);
})}
</div>
</div>
<div className='mt-6 flex items-center justify-center gap-2'>
{deck.map((entry, index) => (
<button
key={`dot-${entry.image.src}-${index}`}
type='button'
aria-label={`Go to slide ${index + 1}`}
onClick={() => jumpToIndex(index)}
className={[
'h-2 rounded-full transition-all duration-300',
index === focusedIndex
? 'w-6 bg-gray-900'
: 'w-2 bg-gray-300 hover:bg-gray-400',
].join(' ')}
/>
))}
</div>
</div>
);
}
```
Works with your AI tools
* Compatible with most projects that use the default React/Next.js framework.
Built With
Props
| Prop | Type | Description |
|---|---|---|
items | GalleryItem[] | Gallery entries — image src, alt, and optional caption |
cardWidth | number | Card width in px |
cardHeight | number | Card height in px |
cornerRadius | number | Border radius in px |
autoplay | boolean | Auto-advance slides (pauses on hover/focus) |
showCaption | boolean | Show the caption on the focused card |
className | string | Styles for the outer shell |
More Gallery Components
Fanned photo arch with center-forward stacking; hover lifts a card and scales it up.






