Vue Guide
Learn how to build sliders with @reelkit/vue.
Basic Slider
The <Reel> component wraps the core slider controller. Use the #item slot to render each slide with virtualization — only visible slides are mounted.
<script setup lang="ts">
import { Reel, ReelIndicator } from '@reelkit/vue';
const items = [
{ title: 'Virtualized', subtitle: 'Only 3 slides in DOM', color: '#6366f1' },
{ title: 'Touch First', subtitle: 'Native swipe gestures', color: '#8b5cf6' },
{ title: 'Zero Deps', subtitle: 'Tiny bundle size', color: '#7c3aed' },
{ title: 'Keyboard Nav', subtitle: 'Full a11y support', color: '#ec4899' },
{ title: 'SSR Ready', subtitle: 'Works everywhere', color: '#14b8a6' },
{ title: '60fps', subtitle: 'Smooth animations', color: '#f59e0b' },
];
const onAfterChange = (index: number) => {
console.log('Current index:', index);
};
</script>
<template>
<Reel
:count="items.length"
style="width: 100%; height: 100dvh"
direction="vertical"
:enable-wheel="true"
@after-change="onAfterChange"
>
<template #item="{ index, size }">
<div
:style="{
width: size[0] + 'px',
height: size[1] + 'px',
background: items[index].color,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
}"
>
<div style="font-size: 1.5rem; font-weight: bold">
{{ items[index].title }}
</div>
<div style="font-size: 0.875rem; opacity: 0.8">
{{ items[index].subtitle }}
</div>
</div>
</template>
<div style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); z-index: 10">
<ReelIndicator direction="vertical" />
</div>
</Reel>
</template>ReelIndicator
Optional component that displays Instagram-style progress indicators showing the current position in the slider. When placed inside a <Reel>, it auto-connects to the parent's count and active values via Vue's provide/inject — no manual wiring needed.
<!-- Auto-connect: count and active are inherited from parent Reel -->
<Reel :count="10" :size="[400, 600]">
<template #item="{ index, size }"> ... </template>
<ReelIndicator direction="vertical" />
</Reel>
<!-- Manual usage: pass count and active explicitly (e.g. outside a Reel) -->
<ReelIndicator :count="10" :active="currentIndex" />Imperative API — Template Ref
The <Reel> component exposes a ReelExpose interface via template ref. Use ref() to store the reference and call imperative methods like next(), prev(), and goTo().
<script setup lang="ts">
import { ref } from 'vue';
import { Reel, type ReelExpose } from '@reelkit/vue';
const items = [
{ title: 'Slide 1', color: '#6366f1' },
{ title: 'Slide 2', color: '#8b5cf6' },
{ title: 'Slide 3', color: '#ec4899' },
];
const sliderRef = ref<ReelExpose | null>(null);
const currentIndex = ref(0);
const onAfterChange = (index: number) => {
currentIndex.value = index;
};
</script>
<template>
<Reel
ref="sliderRef"
:count="items.length"
style="width: 100%; height: 100dvh"
direction="vertical"
:enable-wheel="true"
@after-change="onAfterChange"
>
<template #item="{ index, size }">
<div
:style="{
width: size[0] + 'px',
height: size[1] + 'px',
}"
>
{{ items[index].title }}
</div>
</template>
</Reel>
<div style="position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%)">
<button
:disabled="currentIndex === 0"
@click="sliderRef?.prev()"
>
Prev
</button>
<button
:disabled="currentIndex === items.length - 1"
@click="sliderRef?.next()"
>
Next
</button>
<button @click="sliderRef?.goTo(2)">Go to 3</button>
</div>
</template>Horizontal Direction
Set direction="horizontal" for a left/right swipe slider. The indicator direction should match.
<Reel
:count="items.length"
:size="[400, 300]"
direction="horizontal"
>
<template #item="{ index, size }">
<div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
{{ items[index].title }}
</div>
</template>
<ReelIndicator direction="horizontal" />
</Reel>Auto-sizing
The size prop is optional. When omitted, <Reel> auto-measures its container via ResizeObserver and adapts to CSS-driven layout. The container must be sized by its parent (e.g. flex, grid, or explicit CSS dimensions).
<!-- Explicit size (fixed) -->
<Reel :count="items.length" :size="[400, 600]">
<template #item="{ index, size }"> ... </template>
</Reel>
<!-- Auto-size (responsive — sized by CSS) -->
<Reel :count="items.length" style="width: 100%; height: 100dvh">
<template #item="{ index, size }"> ... </template>
</Reel>Transitions
Pass a transition prop to customize the slide animation. ReelKit ships five tree-shakeable transitions: slideTransition (default), fadeTransition, flipTransition, cubeTransition, and zoomTransition.
<script setup lang="ts">
import { Reel, cubeTransition } from '@reelkit/vue';
</script>
<template>
<Reel
:count="items.length"
:size="[400, 600]"
:transition="cubeTransition"
>
<template #item="{ index, size }"> ... </template>
</Reel>
</template>Loop Mode
Enable infinite circular navigation with the loop prop. The slider wraps seamlessly from the last slide back to the first (and vice versa).
<Reel
:count="items.length"
:size="[400, 600]"
:loop="true"
>
<template #item="{ index, size }"> ... </template>
</Reel>Event Callbacks
The <Reel> component emits several events for tracking slider state:
<script setup lang="ts">
import { Reel } from '@reelkit/vue';
const onBeforeChange = (index: number, nextIndex: number, rangeIndex: number) => {
console.log('Transitioning from', index, 'to', nextIndex);
};
const onAfterChange = (index: number, rangeIndex: number) => {
console.log('Arrived at slide', index);
};
const onSlideDragStart = (index: number) => {
console.log('Started dragging slide', index);
};
const onSlideDragEnd = (index: number) => {
console.log('Stopped dragging slide', index);
};
</script>
<template>
<Reel
:count="20"
style="width: 100%; height: 100dvh"
@before-change="onBeforeChange"
@after-change="onAfterChange"
@slide-drag-start="onSlideDragStart"
@slide-drag-end="onSlideDragEnd"
>
<template #item="{ index, size }"> ... </template>
</Reel>
</template>Navigation
Built-in navigation methods:
- Touch/Swipe: Drag to navigate with momentum and snap
- Keyboard: Arrow keys and Escape
- Mouse Wheel: Enable with
:enable-wheel="true" - Programmatic: Use a template ref to access
next(),prev(),goTo()
<script setup lang="ts">
import { ref } from 'vue';
import { Reel, type ReelExpose } from '@reelkit/vue';
const sliderRef = ref<ReelExpose | null>(null);
</script>
<template>
<Reel
ref="sliderRef"
:count="10"
:size="[400, 600]"
>
<template #item="{ index, size }">
<div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
Slide {{ index + 1 }}
</div>
</template>
</Reel>
<button @click="sliderRef?.prev()">Prev</button>
<button @click="sliderRef?.next()">Next</button>
<button @click="sliderRef?.goTo(5)">Go to 6</button>
</template>URL State
useOverlayUrlState builds a URL-state controller for an overlay and returns it whole, then you hand it to a <LightboxUrlOverlay> as its :controller prop. The address bar owns the open state, so a bound overlay opens itself and a link is the usual open action. The first write of an absent parameter pushes one history entry and every write after replaces it, so paging never buries the back button. Keep the controller to read value/position and to close programmatically with set(null), the same low-level write the overlay uses internally on slide change.
<script setup lang="ts">
import { LightboxUrlOverlay, type LightboxItem } from '@reelkit/vue-lightbox';
import { useOverlayUrlState, urlIndexKey } from '@reelkit/vue';
const props = defineProps<{ images: LightboxItem[] }>();
const photo = useOverlayUrlState({
param: 'photo',
...urlIndexKey(() => props.images.length),
});
</script>
<template>
<!-- Opening is a link — the overlay reads the URL and opens itself. -->
<RouterLink v-for="(img, i) in props.images" :key="img.src" :to="`?photo=${i}`">
<img :src="img.src" />
</RouterLink>
<LightboxUrlOverlay :controller="photo" :items="props.images" />
</template>The options object takes param, codec, and locator (all three required), plus an optional adapter. The codec and locator are a matched pair sharing the same Id, so for a plain ?photo=3 gallery spread ...urlIndexKey(() => props.images.length), which returns both halves at once. urlIndexKey maps the parameter to a slide index and bounds it against the live count the getter returns, so a stale or out-of-range ?photo=99 is rejected and heals itself out of the URL instead of opening a slide that was never named. Pass a getter, not a number: a Vue setup runs once, so a captured length would go stale as a paginated feed grows. It wraps createIndexLocator (the locator half) and pairs it with indexCodec. A paginated feed or an identity-keyed gallery supplies its own matched codec + locator instead. The full options table lives on the Vue API reference.
#item Slot Pattern
Instead of React's render prop, Vue uses the #item scoped slot. This enables virtualization — only visible slides are mounted. The slot scope provides three properties:
<template #item="{ index, indexInRange, size }">
<!--
index : number — absolute slide index (0 to count-1)
indexInRange : number — position in visible window (0, 1, or 2)
size : [number, number] — [width, height] of the container
-->
<MySlide
:data="items[index]"
:style="{ width: size[0] + 'px', height: size[1] + 'px' }"
/>
</template>Composables
@reelkit/vue provides composables for common overlay scenarios:
<script setup lang="ts">
import { ref } from 'vue';
import { useBodyLock, useFullscreen, useReelContext } from '@reelkit/vue';
// Lock body scroll when an overlay is open
const isOpen = ref(true);
useBodyLock(isOpen);
// Fullscreen API with cross-browser support
const containerRef = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen({ elementRef: containerRef });
// Access parent Reel context (when inside a Reel)
const reelContext = useReelContext();
// reelContext?.index — active slide index signal
// reelContext?.count — total slide count signal
// reelContext?.goTo() — navigate programmatically
// Bridge a core Subscribable into a reactive Vue ref
import { toVueRef } from '@reelkit/vue';
const index = toVueRef(reelContext!.index); // Ref<number> — re-renders on change
</script>Key Points
- Composition API
Import
Reel,ReelIndicator, and composables directly into your<script setup>— no plugin registration required - #item scoped slot
The Vue equivalent of React's
itemBuilderprop — enables virtualization with familiar template syntax - Template ref
Use
ref<ReelExpose>()for imperative navigation — no event callbacks required - @after-change
Emits
(index, rangeIndex)— track current index for UI updates - provide/inject context
ReelIndicatorauto-connects to the parentReelvia Vue's provide/inject — no manual prop drilling
Performance Tips
- Keep slide templates lightweight
The
#itemslot runs for each visible slide (typically 3 at a time). Avoid heavy computation or deeply nested structures inside it. - Load data near the edge
Use
@after-changeto detect when the user approaches the end and fetch the next batch before slides run out — enabling infinite scroll feeds. - Use refs for imperative state
Store the
ReelExposereference and current index in Vueref()s for fine-grained reactivity without unnecessary re-renders. - Disable wheel in scrollable pages
Set
:enable-wheel="false"when the slider is embedded in a scrollable layout to avoid capturing the page scroll.
Next Steps
- Vue API Reference - all props, events, and composables
- Core Guide - framework-agnostic engine
- Reel Player - TikTok/Reels-style video playerReel Player - TikTok/Reels-style video playerReel Player - TikTok/Reels-style video player
- Lightbox - image & video galleryLightbox - image & video galleryLightbox - image & video gallery