Carousel
CarouselEngine enhances a native scroll-snap container: it tracks visible and interactive slides,
groups them into pages, manages tabindex/aria for accessibility, mirrors for RTL, and can add
pointer drag with inertia. The browser still does the scrolling — the engine just measures it and
exposes reactive controls.
useCarousel
Fort Camryn
Adsum usitas paulatim suggero.
West Cloyd
Omnis adipiscor pectus assumenda advoco vinum vociferor.
Dunwoody
Color eius incidunt audacia volubilis terga vigor varius.
Grand Island
Illo aqua thorax aestas vos.
North Esta
Colo communis suspendo.
Ameliafurt
Dolor venustas carus cunae temeritas.
Fort Sisterborough
Calcar curso toties.
Fort Jerome
Cinis adfectus peior desolo admoneo.
useCarousel
Fort Camryn
Adsum usitas paulatim suggero.
West Cloyd
Omnis adipiscor pectus assumenda advoco vinum vociferor.
Dunwoody
Color eius incidunt audacia volubilis terga vigor varius.
Grand Island
Illo aqua thorax aestas vos.
North Esta
Colo communis suspendo.
Ameliafurt
Dolor venustas carus cunae temeritas.
Fort Sisterborough
Calcar curso toties.
Fort Jerome
Cinis adfectus peior desolo admoneo.
Drag the cards, use the buttons, or scroll horizontally.
Usage
Mark the scrollable element with a ref and tag each slide with data-slide. useCarousel returns
the engine; useCarouselControls turns it into reactive UI state:
import { useRef } from 'react';
import { useCarousel, useCarouselControls } from '@60fps/react/carousel-engine';
function Gallery() {
const ref = useRef<HTMLDivElement>(null);
const engine = useCarousel(ref);
const controls = useCarouselControls(engine);
return (
<>
<button disabled={!controls.canScrollPrev} onClick={controls.goToPrevPage}>
Prev
</button>
<button disabled={!controls.canScrollNext} onClick={controls.goToNextPage}>
Next
</button>
<div ref={ref} className="relative flex snap-x snap-mandatory gap-4 overflow-x-auto">
{items.map((item) => (
<article key={item.id} data-slide className="shrink-0 snap-start">
…
</article>
))}
</div>
</>
);
}
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { useCarousel, useCarouselControls } from '@60fps/vue/carousel-engine';
const ref = useTemplateRef('carousel');
const engine = useCarousel(ref);
const controls = useCarouselControls(engine);
</script>
<template>
<button :disabled="!controls.canScrollPrev" @click="controls.goToPrevPage">Prev</button>
<div ref="carousel" class="relative flex snap-x snap-mandatory gap-4 overflow-x-auto">
<article v-for="item in items" :key="item.id" data-slide class="shrink-0 snap-start">…</article>
</div>
</template>
Configurations
The options compose freely — a few common setups:
Paged (default)
Slides scroll a page at a time; goToNextPage advances by the number visible together.
useCarousel(ref);
One slide at a time
Set scrollMode: 'slide' so a single slide is the active unit for navigation and accessibility.
useCarousel(ref, { scrollMode: 'slide' });
Start centered
There’s no center option — the carousel only learns its slide count once it measures. You already
know it though, so compute the middle index yourself and pass it as startIndex:
useCarousel(ref, { startIndex: Math.floor((items.length - 1) / 2) });
Pointer drag with inertia
mouseDrag adds click-and-drag scrolling with an inertial release on top of native touch scrolling.
useCarousel(ref, { mouseDrag: true });
Responsive
The carousel adapts to its content. With few enough slides to fit, it doesn’t overflow — there’s
nothing to page through — so canScrollPrev and canScrollNext both report false, the arrows
disable themselves, and it stays a single page. Narrow the window until the six slides overflow and
the controls light back up.
API
useCarousel(ref, options?)
| Option | Type | Default | Description |
|---|---|---|---|
startIndex | number | 0 | Initial slide index. |
mouseDrag | boolean | false | Enable pointer drag with inertial release. |
scrollMode | 'page' | 'slide' | 'page' | `slide` treats one slide as the active unit for a11y/paging. |
updateVisibilityOnScroll | boolean | — | Update aria/tabindex on every scroll, not only at rest. |
getChildren | (el) => slides | — | Override how slides are located. Defaults to `[data-slide]`. |
useCarouselControls(engine)
Reactive snapshot, refreshed on ready / change / rest / resize.
| Member | Type | Default | Description |
|---|---|---|---|
index / setIndex | number / (n) => void | — | Active slide index, controlled. |
pageIndex / setPageIndex | number / (n) => void | — | Active page, where a page is the set of slides visible together. |
pages / pageCount | number[][] / number | — | Slide indexes grouped per page. |
visibleIndexes | number[] | — | Slides at least half in view. |
canScrollPrev / canScrollNext | boolean | — | Whether paging is possible in each direction. |
goToPrevPage / goToNextPage | () => void | — | Page navigation. |
overflows | boolean | — | Whether content actually overflows (carousel is active). |