Skip to content
60fps/ui

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

Fort Camryn

Adsum usitas paulatim suggero.

West Cloyd

West Cloyd

Omnis adipiscor pectus assumenda advoco vinum vociferor.

Dunwoody

Dunwoody

Color eius incidunt audacia volubilis terga vigor varius.

Grand Island

Grand Island

Illo aqua thorax aestas vos.

North Esta

North Esta

Colo communis suspendo.

Ameliafurt

Ameliafurt

Dolor venustas carus cunae temeritas.

Fort Sisterborough

Fort Sisterborough

Calcar curso toties.

Fort Jerome

Fort Jerome

Cinis adfectus peior desolo admoneo.

useCarousel

Fort Camryn

Fort Camryn

Adsum usitas paulatim suggero.

West Cloyd

West Cloyd

Omnis adipiscor pectus assumenda advoco vinum vociferor.

Dunwoody

Dunwoody

Color eius incidunt audacia volubilis terga vigor varius.

Grand Island

Grand Island

Illo aqua thorax aestas vos.

North Esta

North Esta

Colo communis suspendo.

Ameliafurt

Ameliafurt

Dolor venustas carus cunae temeritas.

Fort Sisterborough

Fort Sisterborough

Calcar curso toties.

Fort Jerome

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);
page 1/0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
page 1/0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

One slide at a time

Set scrollMode: 'slide' so a single slide is the active unit for navigation and accessibility.

useCarousel(ref, { scrollMode: 'slide' });
page 1/0
1
2
3
4
5
6
page 1/0
1
2
3
4
5
6

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) });
page 1/0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
page 1/0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

Pointer drag with inertia

mouseDrag adds click-and-drag scrolling with an inertial release on top of native touch scrolling.

useCarousel(ref, { mouseDrag: true });
page 1/0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
page 1/0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

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.

page 1/0
1
2
3
4
5
6
page 1/0
1
2
3
4
5
6

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).