@reelkit/vue-reel-player
| Install | |
|---|---|
npm install @reelkit/vue-reel-player |
|
| Latest Version: | 0.5.2 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 22, 2026 |
| Links: | GitHub · npm |
@reelkit/vue-reel-player
Instagram Reels / TikTok-style video player for Vue 3. Opens as a full-screen overlay with vertical swipe navigation. Handles video autoplay, sound continuity on iOS, and multi-media posts. ~6.2 kB gzip.
Installation
npm install @reelkit/vue-reel-player @reelkit/vue lucide-vue-next
Requires Vue 3.3 or later.
Quick Start
<script setup lang="ts">
import { ref } from 'vue';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import '@reelkit/vue-reel-player/styles.css';
const isOpen = ref(false);
const content: ContentItem[] = [
{
id: '1',
media: [
{
id: 'v1',
type: 'video',
src: 'https://example.com/video.mp4',
poster: 'https://example.com/poster.jpg',
aspectRatio: 9 / 16,
},
],
author: { name: 'John Doe', avatar: 'https://example.com/avatar.jpg' },
likes: 1234,
description: 'Amazing video!',
},
];
</script>
<template>
<button @click="isOpen = true">Open Player</button>
<ReelPlayerOverlay
:is-open="isOpen"
:content="content"
@close="isOpen = false"
/>
</template>
Features
- Vertical swipe navigation (touch, mouse, keyboard, wheel)
- Video autoplay with sound toggle
- Multi-media posts with horizontal nested slider
- Instagram-style indicator dots
- Keyboard navigation (Arrow keys, Escape)
- Desktop navigation arrows
- iOS sound continuity via shared
<video>element - Video position memory across slide changes
- Playback timeline bar over long videos, scrubbable, with buffered ranges
- Shareable URLs —
ReelPlayerUrlOverlayopens itself from the address bar - Themeable via
--rk-reel-*CSS custom properties
Scoped Slots
All of the player's UI is customizable via scoped slots:
| Slot | Scope |
|---|---|
slide |
item, index, size, isActive, slideKey, defaultContent, … |
slideOverlay |
item, index, isActive |
controls |
item, soundState, activeIndex, content, onClose |
navigation |
item, activeIndex, count, onPrev, onNext |
nestedSlide |
item, media, index, size, isActive, isInnerActive, … |
nestedNavigation |
media, activeIndex, count, onPrev, onNext |
timeline |
item, activeIndex, timelineState, defaultContent |
loading |
item, activeIndex |
error |
item, activeIndex |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen |
boolean |
required | Controls overlay visibility |
content |
ContentItem[] |
required | Content items to display |
initialIndex |
number |
0 |
Starting slide index |
initialInnerIndex |
number |
- | Inner media index for the first slide only |
aspectRatio |
number |
9/16 |
Desktop container ratio |
ariaLabel |
string |
'Video player' |
Accessible label announced when the overlay opens |
timeline |
'auto' | 'always' | 'never' |
'auto' |
When the built-in timeline bar renders |
timelineMinDurationSeconds |
number |
30 |
Under 'auto', videos shorter than this get no bar |
loop |
boolean |
false |
Enable infinite loop |
enableNavKeys |
boolean |
true |
Enable keyboard navigation |
enableWheel |
boolean |
true |
Enable mouse wheel navigation |
wheelDebounceMs |
number |
200 |
Wheel debounce duration (ms) |
transitionDuration |
number |
300 |
Transition duration (ms) |
swipeDistanceFactor |
number |
0.12 |
Swipe threshold (0-1) |
Events
| Event | Payload | Description |
|---|---|---|
close |
void |
Emitted when player closes |
slide-change |
number |
Emitted after slide change |
api-ready |
ReelPlayerApi |
Emitted with the imperative player API |
update:is-open |
boolean |
Emitted on close — enables v-model:is-open |
v-model:is-open
Use it to drive the overlay with a single binding instead of pairing
:is-open + @close:
<script setup lang="ts">
import { ref } from 'vue';
const open = ref(false);
</script>
<template>
<button @click="open = true">Open</button>
<ReelPlayerOverlay v-model:is-open="open" :content="content" />
</template>
The legacy :is-open + @close pattern still works.
Custom content types
ReelPlayerOverlay and ReelPlayerUrlOverlay are generic over your content
item shape. Extend BaseContentItem to use any data type: the item type is
inferred from content, so passing anything else is a type error. Vue does not
carry that type into a scoped slot, so annotate the slot binding with the
exported slot-scope type to keep it strongly typed, for example
#slideOverlay="{ item }: SlideOverlaySlotScope<MyItem>":
<script setup lang="ts">
import { ref } from 'vue';
import {
ReelPlayerOverlay,
type BaseContentItem,
type SlideOverlaySlotScope,
} from '@reelkit/vue-reel-player';
interface MyItem extends BaseContentItem {
title: string;
category: 'video' | 'photo';
}
const open = ref(false);
const items: MyItem[] = [
/* ... */
];
</script>
<template>
<ReelPlayerOverlay v-model:is-open="open" :content="items">
<template #slideOverlay="{ item }: SlideOverlaySlotScope<MyItem>">
<div class="my-overlay">
<h2>{{ item.title }}</h2>
<span>{{ item.category }}</span>
</div>
</template>
</ReelPlayerOverlay>
</template>
The same pattern works for every other slot — import the matching scope
type (SlideSlotScope, ControlsSlotScope, NavigationSlotScope,
NestedSlideSlotScope, LoadingSlotScope) and annotate the
destructure.
URL-driven player
ReelPlayerUrlOverlay takes the same props except isOpen — the address bar
owns the open state, so a shared link reopens the same post:
<script setup lang="ts">
import {
ReelPlayerUrlOverlay,
type ContentItem,
} from '@reelkit/vue-reel-player';
import { useOverlayUrlState, urlIndexKey } from '@reelkit/vue';
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';
import '@reelkit/vue-reel-player/styles.css';
const props = defineProps<{ content: ContentItem[] }>();
const reel = useOverlayUrlState({
param: 'reel',
adapter: useVueRouterUrlAdapter(),
...urlIndexKey(() => props.content.length),
});
</script>
<template>
<!-- Opening is a link — the overlay reads the URL and opens itself. -->
<RouterLink
v-for="(post, i) in props.content"
:key="post.id"
:to="`?reel=${i}`"
>
<img :src="https://raw.githubusercontent.com/KonstantinKai/reelkit/HEAD/post.media[0].src" />
</RouterLink>
<ReelPlayerUrlOverlay :controller="reel" :content="props.content" />
</template>
Swap urlIndexKey for urlIndexTwoAxisKey to put the inner media index in the
URL too (?reel=3.2), or urlStableIdKey to key it by post id so a bookmark
survives the feed being reordered. Drop the adapter outside vue-router — the
History API is the default.
Sub-components
For composing your own controls or slides: PlayerControls, CloseButton,
SoundButton, ImageSlide, VideoSlide, SlideOverlay, TimelineBar,
LoadingIndicator, ErrorIndicator, plus TimelineProvider with
useTimelineState /
useTimelineStateOptional to read playback state and useViewportSize for the
desktop/mobile split.
Keyboard Shortcuts
| Key | Action |
|---|---|
ArrowUp |
Previous slide |
ArrowDown |
Next slide |
ArrowLeft |
Previous media (nested) |
ArrowRight |
Next media (nested) |
Escape |
Close player |
Documentation
Docs, runnable StackBlitz examples, and customization guides at reelkit.dev/docs/vue-reel-player.
Support
If ReelKit saved you some time, a star on GitHub would mean a lot — it's a small thing, but it really helps the project get noticed.
License
Related Packages
Full-screen image and video gallery lightbox for Vue 3 with touch gestures, tran...
Instagram-style stories player overlay for Vue 3 with cube transitions and auto-...
Vue 3 components and composables for building TikTok/Instagram Reels-style slide...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.5.2 | >=3.3.0 | MIT | |
| 0.5.1 | >=3.3.0 | MIT | |
| 0.5.0 | >=3.3.0 | MIT | |
| 0.4.2 | >=3.0.0 | MIT | |
| 0.4.1 | >=3.0.0 | MIT | |
| 0.4.0 | >=3.0.0 | MIT | |
| 0.3.2 | >=3.0.0 | MIT | |
| 0.3.1 | >=3.0.0 | MIT | |
| 0.3.0 | >=3.0.0 | MIT | |
| 0.2.1 | >=3.0.0 | MIT | |
| 0.2.0 | >=3.0.0 | MIT | |
| 0.1.0 | >=3.0.0 | MIT |
