Carousel
<div> + CSS translate
A horizontal slide show built with CSS transform: translateX. No dependencies — just a Vue ref for current index and a Tailwind transition-transform.
Overview
Preview
Free Tool
YouTube Thumbnail Downloader
Download any thumbnail in HD instantly.
Free Tool
Channel Stats Viewer
Full channel analytics at a glance.
Coming Soon
More tools coming
New free tools added regularly.
Core pattern
html
<!-- Outer clips overflow; inner translates -->
<div class="relative overflow-hidden rounded-xl">
<div
class="flex transition-transform duration-300 ease-in-out"
:style="{ transform: `translateX(-${current * 100}%)` }"
>
<div v-for="slide in slides" :key="slide.id" class="w-full shrink-0">
<!-- slide content -->
</div>
</div>
</div>Each slide is w-full shrink-0. The track shifts left by current * 100% to reveal the active slide.
Prev / Next buttons
html
<button
@click="prev"
:disabled="current === 0"
class="absolute left-2 top-1/2 -translate-y-1/2 flex h-8 w-8 items-center justify-center
rounded-full border border-zinc-700/60 bg-zinc-900/80 text-zinc-300
hover:bg-zinc-800 transition-colors backdrop-blur disabled:opacity-30 disabled:pointer-events-none"
aria-label="Previous slide"
>‹</button>
<button
@click="next"
:disabled="current === slides.length - 1"
class="absolute right-2 top-1/2 -translate-y-1/2 ..."
aria-label="Next slide"
>›</button>Dot indicators
html
<div class="flex items-center justify-center gap-1.5 mt-3" role="tablist" aria-label="Slides">
<button
v-for="(_, i) in slides"
:key="i"
@click="current = i"
role="tab"
:aria-selected="i === current"
:aria-label="`Slide ${i + 1}`"
class="rounded-full transition-all duration-200"
:class="i === current ? 'w-5 h-1.5 bg-[#D40C37]' : 'w-1.5 h-1.5 bg-zinc-700 hover:bg-zinc-500'"
></button>
</div>The active dot stretches to w-5 (pill shape) as a clear position indicator.
Thumbnail strip
html
<div class="flex gap-2 overflow-x-auto mt-3">
<button
v-for="(slide, i) in slides"
:key="slide.id"
@click="current = i"
class="shrink-0 overflow-hidden rounded-lg border-2 h-14 w-20 transition-all"
:class="i === current ? 'border-[#D40C37]' : 'border-transparent opacity-50 hover:opacity-75'"
:aria-label="`Go to slide: ${slide.title}`"
>
<!-- thumbnail image or colour -->
</button>
</div>Full Vue implementation
vue
<script setup>
import { ref } from 'vue'
const current = ref(0)
const slides = [
{ id: 1, title: 'Slide One', img: '/slides/1.jpg' },
{ id: 2, title: 'Slide Two', img: '/slides/2.jpg' },
{ id: 3, title: 'Slide Three', img: '/slides/3.jpg' },
]
function prev() { if (current.value > 0) current.value-- }
function next() { if (current.value < slides.length - 1) current.value++ }
</script>Autoplay
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const current = ref(0)
let timer = null
function startAutoplay() {
timer = setInterval(() => {
current.value = (current.value + 1) % slides.length
}, 4000)
}
function stopAutoplay() { clearInterval(timer) }
onMounted(startAutoplay)
onUnmounted(stopAutoplay)
</script>
<template>
<!-- pause on hover -->
<div @mouseenter="stopAutoplay" @mouseleave="startAutoplay" class="relative overflow-hidden ...">
<!-- carousel -->
</div>
</template>Looping (infinite)
vue
function next() { current.value = (current.value + 1) % slides.length }
function prev() { current.value = (current.value - 1 + slides.length) % slides.length }
// Remove :disabled from buttonsAccessibility notes
- Wrap the carousel in a
<section aria-label="Featured content" aria-roledescription="carousel"> - Each slide should have
role="group"andaria-roledescription="slide"witharia-label="Slide N of M" - Pause autoplay on hover AND when the user prefers reduced motion:
@media (prefers-reduced-motion: reduce) - Dot/thumbnail controls act as tabs — use
role="tablist"/role="tab"/aria-selected