Basic Carousel
A slide carousel with prev/next arrows, dots and a polite live region.
6 frameworksIntermediate
The active card sits centred and full-size while its neighbours peek and dim.
<!-- Center mode: the active card sits centred at 70% width with the neighbours
peeking and dimmed. The track offset is calc(15% - index*70%) - 15% is half
of the 30% a 70% card leaves, so the current card lands dead centre. -->
<section class="mx-auto max-w-2xl" aria-roledescription="carousel" aria-label="Highlights" data-cm>
<div class="overflow-hidden">
<div class="flex items-center transition-transform duration-500 ease-out motion-reduce:transition-none" style="transform: translateX(15%);" data-cm-track>
<div class="shrink-0 basis-[70%] px-2 transition-all duration-500 motion-reduce:transition-none" role="group" aria-roledescription="slide" aria-label="1 of 3">
<div class="flex h-40 flex-col justify-end rounded-xl bg-gradient-to-br from-blue-600 to-indigo-600 p-5 text-white"><h3 class="text-lg font-semibold">Analytics</h3><p class="text-sm text-white/90">Every metric that matters.</p></div>
</div>
<div class="shrink-0 basis-[70%] px-2 scale-90 opacity-50 transition-all duration-500 motion-reduce:transition-none" role="group" aria-roledescription="slide" aria-label="2 of 3" aria-hidden="true">
<div class="flex h-40 flex-col justify-end rounded-xl bg-gradient-to-br from-indigo-600 to-violet-600 p-5 text-white"><h3 class="text-lg font-semibold">Automations</h3><p class="text-sm text-white/90">Turn a click into a rule.</p></div>
</div>
<div class="shrink-0 basis-[70%] px-2 scale-90 opacity-50 transition-all duration-500 motion-reduce:transition-none" role="group" aria-roledescription="slide" aria-label="3 of 3" aria-hidden="true">
<div class="flex h-40 flex-col justify-end rounded-xl bg-gradient-to-br from-teal-700 to-sky-700 p-5 text-white"><h3 class="text-lg font-semibold">Integrations</h3><p class="text-sm text-white/90">Forty connectors.</p></div>
</div>
</div>
</div>
<div class="mt-3 flex items-center justify-center gap-4">
<button class="grid h-9 w-9 place-items-center rounded-full border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:hover:text-white dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950" type="button" aria-label="Previous slide" data-cm-prev>
<svg class="h-[1.125rem] w-[1.125rem]" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m12.5 5-5 5 5 5" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
<button class="grid h-9 w-9 place-items-center rounded-full border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:hover:text-white dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950" type="button" aria-label="Next slide" data-cm-next>
<svg class="h-[1.125rem] w-[1.125rem]" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m7.5 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
</div>
</section>
<script>
(function () {
document.querySelectorAll('[data-cm]').forEach(function (root) {
var track = root.querySelector('[data-cm-track]');
var slides = Array.prototype.slice.call(track.children);
var index = 0;
function go(next) {
index = (next + slides.length) % slides.length;
track.style.transform = 'translateX(calc(15% - ' + index * 70 + '%))';
slides.forEach(function (s, i) {
if (i === index) { s.classList.remove('scale-90', 'opacity-50'); s.removeAttribute('aria-hidden'); }
else { s.classList.add('scale-90', 'opacity-50'); s.setAttribute('aria-hidden', 'true'); }
});
}
root.querySelector('[data-cm-prev]').addEventListener('click', function () { go(index - 1); });
root.querySelector('[data-cm-next]').addEventListener('click', function () { go(index + 1); });
});
})();
</script>| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | Slide[] | - | The array of items to render. |
className | string | - | Additional classes merged onto the root element. |
ariaLabel | string | 'Carousel' | The button's accessible name. Required for icon-only buttons. |
The track offset is `calc(15% - index*70%)`: 15% is half the space a 70%-wide card leaves, so the current card lands dead centre. Inactive cards scale to 90% at 50% opacity, giving a coverflow feel with transforms alone.