Carousel Components
Beautiful, responsive carousels built with TailwindCSS and AlpineJS. Perfect for hero sliders and image galleries.
1. Basic Carousel
Minimal AlpineJS carousel with autoplay, hover-pause, keyboard arrows, touch swipe, and pagination dots.
<!-- AlpineJS Carousel (HTML + Tailwind) -->
<div
x-data="carousel({ interval: 5000 })"
x-init="init()"
@keydown.window="onKeydown"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@touchstart.passive="touchstart($event)"
@touchend.passive="touchend($event)"
class="relative w-full overflow-hidden rounded-2xl bg-black/5 ring-1 ring-black/5"
role="region" aria-roledescription="carousel" aria-label="Feature carousel"
>
<div class="relative h-96">
<template x-for="(slide, idx) in slides" :key="slide.id">
<div x-show="current === idx" x-transition
class="absolute inset-0" role="group" :aria-label="`Slide ${idx+1} of ${slides.length}`">
<img :src="slide.img" :alt="slide.alt" class="h-full w-full object-cover">
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-6 text-white">
<h3 class="text-xl font-semibold" x-text="slide.title"></h3>
<p class="mt-1 text-sm opacity-90" x-text="slide.caption"></p>
</div>
</div>
</template>
</div>
<!-- Controls -->
<button @click="prev" class="group absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">…</button>
<button @click="next" class="group absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">…</button>
<!-- Dots -->
<div class="absolute bottom-3 inset-x-0 flex justify-center gap-2">
<template x-for="(slide, i) in slides" :key="slide.id">
<button @click="go(i)" class="h-2.5 w-2.5 rounded-full"
:class="current === i ? 'bg-white' : 'bg-white/50 hover:bg-white/80'"></button>
</template>
</div>
</div>
<!-- Alpine helper (include once) -->
<script>
// see real implementation in the page's <script> (alpine:init)
</script>
<template>
<div class="relative w-full overflow-hidden rounded-2xl bg-black/5 ring-1 ring-black/5" role="region" aria-roledescription="carousel">
<div class="relative h-96">
<div v-for="(s,i) in slides" :key="s.id" v-show="current===i" class="absolute inset-0">
<img :src="s.img" :alt="s.alt" class="h-full w-full object-cover" />
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-6 text-white">
<h3 class="text-xl font-semibold">{{ s.title }}</h3>
<p class="mt-1 text-sm opacity-90">{{ s.caption }}</p>
</div>
</div>
</div>
<button @click="prev" class="absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">‹</button>
<button @click="next" class="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">›</button>
<div class="absolute bottom-3 inset-x-0 flex justify-center gap-2">
<button v-for="(s,i) in slides" :key="s.id" @click="go(i)" class="h-2.5 w-2.5 rounded-full" :class="current===i?'bg-white':'bg-white/50 hover:bg-white/80'"/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const current = ref(0)
const slides = ref([
{ id:1, title:'Misty Mountains', caption:'Sunrise in the valley', alt:'', img:'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1600&auto=format&fit=crop' },
{ id:2, title:'Ocean Breeze', caption:'Waves for days', alt:'', img:'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=1600&auto=format&fit=crop' },
{ id:3, title:'City Nights', caption:'Neon lights', alt:'', img:'https://images.unsplash.com/photo-1508057198894-247b23fe5ade?q=80&w=1600&auto=format&fit=crop' },
])
let timer; const interval = 5000
const go = (i) => current.value = (i + slides.value.length) % slides.value.length
const next = () => go(current.value + 1)
const prev = () => go(current.value - 1)
onMounted(() => { timer = setInterval(next, interval) })
onBeforeUnmount(() => clearInterval(timer))
</script>
import React, { useEffect, useRef, useState } from 'react'
export default function BasicCarousel() {
const [current, setCurrent] = useState(0)
const slides = [
{ id:1, title:'Misty Mountains', caption:'Sunrise in the valley', alt:'', img:'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1600&auto=format&fit=crop' },
{ id:2, title:'Ocean Breeze', caption:'Waves for days', alt:'', img:'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=1600&auto=format&fit=crop' },
{ id:3, title:'City Nights', caption:'Neon lights', alt:'', img:'https://images.unsplash.com/photo-1508057198894-247b23fe5ade?q=80&w=1600&auto=format&fit=crop' }
]
const len = slides.length
const go = (i) => setCurrent((i + len) % len)
const next = () => go(current + 1)
const prev = () => go(current - 1)
const timer = useRef(null)
useEffect(() => {
timer.current = setInterval(next, 5000)
return () => clearInterval(timer.current)
}, [current])
return (
<div className="relative w-full overflow-hidden rounded-2xl bg-black/5 ring-1 ring-black/5">
<div className="relative h-96">
{slides.map((s, i) => (
<div key={s.id} className={`absolute inset-0 ${current===i?'opacity-100':'opacity-0'} transition-opacity duration-500`}>
<img src={s.img} alt={s.alt} className="h-full w-full object-cover" />
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-6 text-white">
<h3 className="text-xl font-semibold">{s.title}</h3>
<p className="mt-1 text-sm opacity-90">{s.caption}</p>
</div>
</div>
))}
</div>
<button onClick={prev} className="absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">‹</button>
<button onClick={next} className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">›</button>
<div className="absolute bottom-3 inset-x-0 flex justify-center gap-2">
{slides.map((_, i) => (
<button key={i} onClick={() => go(i)} className={`h-2.5 w-2.5 rounded-full ${current===i?'bg-white':'bg-white/50 hover:bg-white/80'}`} />
))}
</div>
</div>
)
}
2. Carousel with Thumbnails & Progress
Autoplay carousel with hover-pause, swipe, arrow keys, clickable thumbnails, and a progress bar.
<!-- Carousel with Thumbnails & Progress (HTML + Alpine) -->
<div x-data="thumbCarousel({ interval: 4500 })" x-init="init()" class="relative w-full rounded-2xl bg-black/5 ring-1 ring-black/5 overflow-hidden" role="region" aria-roledescription="carousel">
<div class="relative h-96">
<template x-for="(slide, idx) in slides" :key="slide.id">
<div x-show="current === idx" x-transition class="absolute inset-0">
<img :src="slide.img" :alt="slide.alt" class="h-full w-full object-cover">
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-6 text-white">
<h3 class="text-xl font-semibold" x-text="slide.title"></h3>
<p class="mt-1 text-sm opacity-90" x-text="slide.caption"></p>
</div>
</div>
</template>
<div class="absolute left-0 right-0 bottom-0 h-1.5 bg-black/30">
<div class="h-full bg-white/80" :style="{ width: progress + '%' }"></div>
</div>
</div>
<button @click="prev" class="absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">‹</button>
<button @click="next" class="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">›</button>
<div class="flex items-center gap-3 overflow-x-auto p-4 bg-white/70 backdrop-blur">
<template x-for="(slide, i) in slides" :key="slide.id">
<button @click="go(i)" class="flex-shrink-0">
<img :src="slide.img" :alt="slide.alt" class="h-14 w-20 object-cover rounded-lg"
:class="current === i ? 'ring-2 ring-violet' : 'ring-1 ring-black/10 opacity-80 hover:opacity-100'">
</button>
</template>
</div>
</div>
<template>
<div class="relative w-full rounded-2xl bg-black/5 ring-1 ring-black/5 overflow-hidden" role="region" aria-roledescription="carousel">
<div class="relative h-96">
<div v-for="(s,i) in slides" :key="s.id" v-show="current===i" class="absolute inset-0">
<img :src="s.img" :alt="s.alt" class="h-full w-full object-cover" />
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-6 text-white">
<h3 class="text-xl font-semibold">{{ s.title }}</h3>
<p class="mt-1 text-sm opacity-90">{{ s.caption }}</p>
</div>
</div>
<div class="absolute left-0 right-0 bottom-0 h-1.5 bg-black/30">
<div class="h-full bg-white/80" :style="{ width: progress + '%' }"></div>
</div>
</div>
<button @click="prev" class="absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">‹</button>
<button @click="next" class="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">›</button>
<div class="flex items-center gap-3 overflow-x-auto p-4 bg-white/70 backdrop-blur">
<button v-for="(s,i) in slides" :key="s.id" @click="go(i)" class="flex-shrink-0">
<img :src="s.img" :alt="s.alt" class="h-14 w-20 object-cover rounded-lg"
:class="current===i ? 'ring-2 ring-violet' : 'ring-1 ring-black/10 opacity-80 hover:opacity-100'" />
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const current = ref(0)
const progress = ref(0)
const slides = ref([
{ id:1, title:'Misty Mountains', caption:'Sunrise in the valley', alt:'', img:'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1600&auto=format&fit=crop' },
{ id:2, title:'Ocean Breeze', caption:'Waves for days', alt:'', img:'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=1600&auto=format&fit=crop' },
{ id:3, title:'City Nights', caption:'Neon lights', alt:'', img:'https://images.unsplash.com/photo-1508057198894-247b23fe5ade?q=80&w=1600&auto=format&fit=crop' },
])
const len = slides.value.length
const go = (i) => { current.value = (i + len) % len; resetProgress() }
const next = () => go(current.value + 1)
const prev = () => go(current.value - 1)
let slideTimer, progressTimer
const interval = 4500, step = 50
function resetProgress() { progress.value = 0 }
onMounted(() => {
slideTimer = setInterval(next, interval)
progressTimer = setInterval(() => {
progress.value = Math.min(100, progress.value + (step/interval)*100)
}, step)
})
onBeforeUnmount(() => { clearInterval(slideTimer); clearInterval(progressTimer) })
</script>
import React, { useEffect, useRef, useState } from 'react'
export default function ThumbCarousel() {
const [current, setCurrent] = useState(0)
const [progress, setProgress] = useState(0)
const slides = [
{ id:1, title:'Misty Mountains', caption:'Sunrise in the valley', alt:'', img:'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1600&auto=format&fit=crop' },
{ id:2, title:'Ocean Breeze', caption:'Waves for days', alt:'', img:'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=1600&auto=format&fit=crop' },
{ id:3, title:'City Nights', caption:'Neon lights', alt:'', img:'https://images.unsplash.com/photo-1508057198894-247b23fe5ade?q=80&w=1600&auto=format&fit=crop' }
]
const len = slides.length
const go = (i) => { setCurrent((i + len) % len); setProgress(0) }
const next = () => go(current + 1)
const prev = () => go(current - 1)
const slideTimer = useRef(null)
const progressTimer = useRef(null)
useEffect(() => {
slideTimer.current = setInterval(next, 4500)
progressTimer.current = setInterval(() => {
setProgress(p => Math.min(100, p + (50/4500)*100))
}, 50)
return () => { clearInterval(slideTimer.current); clearInterval(progressTimer.current) }
}, [current])
return (
<div className="relative w-full rounded-2xl bg-black/5 ring-1 ring-black/5 overflow-hidden">
<div className="relative h-96">
{slides.map((s, i) => (
<div key={s.id} className={`absolute inset-0 ${current===i?'opacity-100':'opacity-0'} transition-opacity duration-500`}>
<img src={s.img} alt={s.alt} className="h-full w-full object-cover" />
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-6 text-white">
<h3 className="text-xl font-semibold">{s.title}</h3>
<p className="mt-1 text-sm opacity-90">{s.caption}</p>
</div>
</div>
))}
<div className="absolute left-0 right-0 bottom-0 h-1.5 bg-black/30">
{/* Escape Blade on the double-curly style prop: */}
<div className="h-full bg-white/80" style={{ width: `\${progress}%` }} />
</div>
</div>
<button onClick={prev} className="absolute left-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">‹</button>
<button onClick={next} className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full bg-white/70 p-2 ring-1 ring-black/10">›</button>
<div className="flex items-center gap-3 overflow-x-auto p-4 bg-white/70 backdrop-blur">
{slides.map((s, i) => (
<button key={s.id} onClick={() => go(i)} className="flex-shrink-0">
<img src={s.img} alt={s.alt} className={`h-14 w-20 object-cover rounded-lg ${current===i ? 'ring-2 ring-violet' : 'ring-1 ring-black/10 opacity-80 hover:opacity-100'}`} />
</button>
))}
</div>
</div>
)
}
🚀 Quick Tips
- Replace the
slides
arrays with your own images/text. - Autoplay pauses on hover and resumes on mouse leave; arrow keys also work.
- If Alpine throws “XYZ is not defined”, ensure this file loads after Alpine and keep the helpers inside the
alpine:init
listener (as done above).