Skip to content
60fps/ui
@60fps/ui-carouselv0.0.3

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 Chauncey

Fort Chauncey

Adsum usitas paulatim suggero.

West Ciara

West Ciara

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 Erick

North Erick

Colo communis suspendo.

Alexandrafurt

Alexandrafurt

Dolor venustas carus cunae temeritas.

Fort Sylvesterborough

Fort Sylvesterborough

Calcar curso toties.

Fort Jared

Fort Jared

Cinis adfectus peior desolo admoneo.

useCarousel

Fort Chauncey

Fort Chauncey

Adsum usitas paulatim suggero.

West Ciara

West Ciara

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 Erick

North Erick

Colo communis suspendo.

Alexandrafurt

Alexandrafurt

Dolor venustas carus cunae temeritas.

Fort Sylvesterborough

Fort Sylvesterborough

Calcar curso toties.

Fort Jared

Fort Jared

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/ui-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/ui-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>

Controls anywhere in the tree

Navigation often lives deep inside the carousel markup — arrows overlaid on the slides, dots in a footer, a “next” CTA inside a slide. Instead of drilling the controls down as props, both adapters expose them through context, each with its framework’s idiom: in React, wrap the carousel with CarouselProvider (it calls useCarousel + useCarouselControls for you) and read useCarouselContext() from any descendant — or the narrower useCarouselControlsContext() / useCarouselEngineContext(). In Vue, useCarouselControls already provides its controls — no wrapper component needed, descendants inject(carouselSymbol).

import { useRef } from 'react';
import { CarouselProvider, useCarouselContext } from '@60fps/ui-react/carousel-engine';

function NextButton() {
	// Works anywhere under <CarouselProvider> — no props needed.
	const { controls } = useCarouselContext();
	return (
		<button disabled={!controls.canScrollNext} onClick={controls.goToNextPage}>
			Next
		</button>
	);
}

function Gallery() {
	const ref = useRef<HTMLDivElement>(null);
	return (
		<CarouselProvider elementRef={ref} mouseDrag>
			<div ref={ref} className="flex snap-x snap-mandatory gap-4 overflow-x-auto">
				{items.map((item) => (
					<article key={item.id} data-slide>

					</article>
				))}
			</div>
			<NextButton />
		</CarouselProvider>
	);
}
<!-- NextButton.vue — works anywhere under the component that called useCarouselControls -->
<script setup lang="ts">
import { inject } from 'vue';
import { carouselSymbol } from '@60fps/ui-vue/carousel-engine';

const controls = inject(carouselSymbol)!;
</script>

<template>
	<button :disabled="!controls.canScrollNext" @click="controls.goToNextPage">Next</button>
</template>
ExportTypeDefaultDescription
CarouselProviderReact componentCreates the engine for elementRef (same options as useCarousel) and provides engine + controls.
useCarouselContext()React hook{ engine, controls } from the nearest provider. Throws outside one.
useCarouselControlsContext() / useCarouselEngineContext()React hooksNarrower variants when a component needs only one of the two.
carouselSymbolVue InjectionKeyInjection key for the controls that useCarouselControls provides automatically.

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

OptionTypeDefaultDescription
startIndexnumber0Initial slide index.
mouseDragbooleanfalseEnable pointer drag with inertial release.
scrollMode'page' | 'slide''page'`slide` treats one slide as the active unit for a11y/paging.
updateVisibilityOnScrollbooleanUpdate aria/tabindex on every scroll, not only at rest.
getChildren(el) => slidesOverride how slides are located. Defaults to `[data-slide]`.

useCarouselControls(engine)

Reactive snapshot, refreshed on ready / change / rest / resize.

MemberTypeDefaultDescription
index / setIndexnumber / (n) => voidActive slide index, controlled.
pageIndex / setPageIndexnumber / (n) => voidActive page, where a page is the set of slides visible together.
pages / pageCountnumber[][] / numberSlide indexes grouped per page.
visibleIndexesnumber[]Slides at least half in view.
canScrollPrev / canScrollNextbooleanWhether paging is possible in each direction.
goToPrevPage / goToNextPage() => voidPage navigation.
overflowsbooleanWhether content actually overflows (carousel is active).