Vue Reel Player

Full-screen Instagram/TikTok-style vertical media player for Vue 3, built on @reelkit/vue-reel-player.

View live demo →

Features

Vertical Swipe
Touch, drag, keyboard, wheel
Video Autoplay
Plays when visible
Sound Toggle
iOS continuity
Multi-Media
Horizontal nested carousels
Position Memory
Resumes where left off
Frame Capture
Poster-to-video crossfade
Virtualized
Only 3 slides in DOM
Aspect Ratio
9:16 desktop, full mobile
Desktop Nav
Arrow buttons
Generic Types
Custom content data models
Scoped Slots
Customize every UI element
v-model:is-open
Two-way binding on visibility

Installation

bash

Import the stylesheet once in your app entry (or any component):

typescript
Icons
The default controls use lucide-vue-next for icons (close, sound, navigation arrows). If you prefer a different icon library, use the #controls and #navigation scoped slots to provide your own.

Quick Start

Render a grid of thumbnails and open the overlay at the clicked index. Binding v-model:is-open means the parent ref stays in sync when the user closes the player via button, gesture, or Escape.

App.vue

API Reference

Props

PropTypeDefaultDescription
ariaLabelstring'Video player'Accessible label for the dialog region; announced by screen readers when the overlay opens
aspectRationumber9 / 16Width/height ratio for the desktop container. Mobile uses the full viewport.
contentT[] (extends BaseContentItem)requiredArray of content items to display in the player
enableNavKeysbooleantrueEnable keyboard arrow-key navigation
enableWheelbooleantrueEnable mouse-wheel navigation
initialIndexnumber0Zero-based index of the initially visible item
isOpenbooleanrequiredControls overlay visibility; when false the overlay is removed from the DOM
loopbooleanfalseEnable infinite loop between slides
swipeDistanceFactornumber0.12Minimum swipe distance fraction to trigger a slide change
timeline'auto' | 'always' | 'never''auto'Gating strategy for the built-in playback timeline bar. 'auto' renders only for videos longer than timelineMinDurationSeconds; 'always' renders whenever the active slide has a video; 'never' disables the built-in bar (use the #timeline slot for a fully custom replacement).
timelineMinDurationSecondsnumber30Minimum video duration (seconds) for timeline='auto' to render the built-in bar. Short looping clips below this threshold are suppressed.
transitionDurationnumber300Slide animation duration in ms
wheelDebounceMsnumber200Debounce duration for wheel events in ms

Events

EventPayloadDescription
@api-readyReelApiEmitted once the slider is ready, exposing the imperative API
@closevoidEmitted when the player closes
@slide-changenumberEmitted with the new active slide index after a change
@update:is-openbooleanEmitted on close; enables `v-model:is-open`

v-model:is-open

Use v-model:is-open to drive the overlay with a single binding. The legacy :is-open + @close pattern still works if you need the explicit event.

vue

Scoped Slots

Eight scoped slots let you replace any part of the player UI. Each receives a strongly-typed scope object. Slots you don't pass fall back to the defaults.

SlotScopeDescription
#controls{ item, soundState, activeIndex, content, onClose }Custom global controls bar (close, sound, share, etc.)
#error{ item, activeIndex, innerActiveIndex }Custom error indicator (replaces the default icon)
#loading{ item, activeIndex, innerActiveIndex }Custom loading indicator (replaces the default wave loader)
#navigation{ item, activeIndex, count, onPrev, onNext }Custom prev/next navigation arrows (desktop)
#nestedNavigation{ media, activeIndex, count, onPrev, onNext }Custom arrows for the inner horizontal slider
#nestedSlide{ item, media, index, size, isActive, isInnerActive, slideKey, defaultContent, onReady, onWaiting, onError }Custom slide content inside the inner horizontal slider
#slide{ item, index, size, isActive, slideKey, defaultContent, onReady, onWaiting, onError }Fully custom slide content (falls back to default if omitted)
#slideOverlay{ item, index, isActive }Per-slide overlay (author info, likes, description, etc.)
#timeline{ item, activeIndex, timelineState, defaultContent }Custom playback timeline bar. Only invoked when the built-in gate (timeline mode + min duration) would render the default bar; reuses the same auto/always/never logic. Use defaultContent() to wrap the built-in <TimelineBar />.
vue

Custom Timeline

Replace the built-in playback bar with your own scrub UI via the #timeline slot. The slot fires only when the overlay's gating rules would render the default bar (same timeline mode + timelineMinDurationSeconds), so you don't re-implement it. Reuse the .rk-reel-timeline class on your root to inherit flush-bottom positioning, safe-area padding, and touch-device clearance.

vue

Custom Content Types

ReelPlayerOverlay is generic over your content item shape. Extend BaseContentItem to use any data model, and import the matching slot-scope type to keep slot bindings strongly typed:

vue

The same pattern works for every other slot. Import the matching scope type (SlideSlotScope, ControlsSlotScope, NavigationSlotScope, NestedSlideSlotScope, LoadingSlotScope) and annotate the destructure.

Types

ContentItem

FieldTypeDescription
idstringUnique identifier
mediaMediaItem[]One or more media assets (image or video)
author{ name: string; avatar?: string }Author shown in the default slide overlay
descriptionstring?Caption text
likesnumber?Likes count

TimelineBarProps

typescript

TimelineSlotScope<T>

typescript

MediaItem

FieldTypeDescription
idstringUnique identifier
type'image' | 'video'Media type
srcstringURL of the media asset
posterstring?Poster thumbnail URL for video items
aspectRationumberwidth/height ratio. Values < 1 = vertical (cover), ≥ 1 = horizontal (contain).

Sub-Components

Drop these into your custom #controls, #slide, or #slideOverlay templates. Pass the dimensions and callbacks through from the slot scope so autoplay, poster capture, and sound sync keep working.

CloseButton

Standalone circular close button with default reel-player styling. Use inside #controls.

vue

SoundButton

Mute/unmute toggle. Render it inside a SoundProvider (ReelPlayerOverlay provides one). Hidden when the active slide has no video.

vue

TimelineBar

Default playback scrub bar. Reads from the nearest TimelineProvider (automatically mounted inside ReelPlayerOverlay) and renders the track, buffered ranges, progress fill, and scrub pill. Theme via the --rk-reel-timeline-* custom properties, or replace via the #timeline slot.

vue

SlideOverlay

The default gradient overlay showing author, description, and likes. Renders when content carries those fields. Replace or hide it via the #slideOverlay slot.

vue

ImageSlide

Image slide with lazy loading and object-fit: cover by default. Compose it inside the #slide slot to customize image rendering while keeping built-in behavior.

vue

VideoSlide

Video slide backed by a shared <video> element. Handles iOS sound continuity, poster frames, and position memory. Render it inside a SoundProvider (ReelPlayerOverlay provides one).

vue
Composing custom slides
Use #slide with ImageSlide / VideoSlide to customize media rendering while keeping all built-in behavior (autoplay, poster capture, sound sync).
vue

Content Loading & Error Handling

The player tracks per-slide loading and error states. A wave loader shows while content loads; broken media shows an error icon. The player caches failed URLs, so reopening a broken slide skips the retry.

Lifecycle Callbacks

When using the #slide slot, call these callbacks from the slot scope to drive the loading indicator:

CallbackWhen to call
onReadyImage loaded or video started playing. Clears loading and error states.
onWaitingVideo is buffering mid-playback. Shows the loading indicator.
onErrorContent failed to load. Shows error overlay and caches the URL as broken.
vue

Custom Loading & Error UI

Replace the default wave loader and error icon via the #loading and #error slots:

vue

Timeline

The overlay renders a built-in playback timeline bar over the active video. Gate it via the timeline prop: 'auto' (default) renders whenever the active media is a video longer than timelineMinDurationSeconds (default 30), 'always' whenever a video is active, 'never' to disable. For a fully custom scrub bar, use the #timeline slot; its scope exposes a timelineState backed by the underlying TimelineController.

vue

Theme via the --rk-reel-timeline-* CSS custom properties.

Sound Context

ReelPlayerOverlay mounts a SoundProvider at its root, so any component rendered inside can read or toggle mute state via useSoundState. The composable re-exports from @reelkit/vue-reel-player so you don't need a separate @reelkit/vue import.

vue
Inside the player, the #controls slot also exposes soundState on its scope. Prefer that when you only need it inside the controls template.

CSS Classes

CSS classes are plain (not scoped). A stylesheet loaded after @reelkit/vue-reel-player/styles.css can override any of them with a higher-specificity selector. For color, size, and z-index changes, use the CSS custom properties in the Theming section below.

ClassComponentDescription
.rk-reel-overlayOverlayFixed full-screen backdrop (background, z-index)
.rk-reel-containerOverlayPlayer container (position, overflow)
.rk-reel-loaderOverlayWave loading animation overlay
.rk-reel-media-errorOverlayError state overlay (centered icon + text)
.rk-reel-media-error-textOverlayError message text
.rk-reel-buttonControlsShared circular icon button (close, sound, nav arrows)
.rk-reel-close-btnControlsClose button
.rk-reel-sound-btnControlsSound toggle button
.rk-reel-nav-arrowsNavigationDesktop-only arrow container (hidden below 768px)
.rk-reel-nav-buttonNavigationIndividual prev/next nav arrow
.rk-reel-slide-wrapperSlideWrapper around media + overlay
.rk-reel-slide-overlaySlideOverlayGradient overlay container
.rk-reel-slide-overlay-authorSlideOverlayAuthor row (avatar + name)
.rk-reel-slide-overlay-avatarSlideOverlayAuthor avatar image
.rk-reel-slide-overlay-nameSlideOverlayAuthor name text
.rk-reel-slide-overlay-descriptionSlideOverlayDescription text
.rk-reel-slide-overlay-likesSlideOverlayLikes row (heart + count)
.rk-reel-video-containerVideoSlideVideo wrapper (background, overflow)
.rk-reel-video-elementVideoSlideThe <video> element
.rk-reel-video-posterVideoSlidePoster image (fades out on play)
.rk-reel-video-poster.rk-visibleVideoSlideState modifier applied to the poster while the video is paused/loading
.rk-reel-nested-indicatorNestedSliderDot pagination under multi-media slides (position varies desktop vs. touch)
.rk-reel-nested-navNestedSliderHorizontal carousel arrows (hidden below 768px)
.rk-reel-nested-nav-nextNestedSliderNested next arrow position
.rk-reel-nested-nav-prevNestedSliderNested prev arrow position
.rk-reel-timelineTimelineBarScrub-bar wrapper. Reuse on custom `#timeline` slot roots to inherit flush-bottom positioning, safe-area padding, and touch-device slide-overlay clearance.
.rk-reel-timeline-trackTimelineBarTrack (unplayed region)
.rk-reel-timeline-bufferedTimelineBarBuffered segments layer
.rk-reel-timeline-fillTimelineBarPlayed-progress fill
.rk-reel-timeline-cursorTimelineBarScrub-handle pill (floats above the track)

Theming

Every color, size, z-index, and transition lives in a CSS custom property. Override one or many at :root (or any ancestor of the overlay) to retheme without touching component source. The tokens match @reelkit/react-reel-player, so overrides port between bindings.

TokenDefaultControls
--rk-reel-overlay-bg#000Full-screen backdrop color
--rk-reel-overlay-z1000Overlay z-index
--rk-reel-button-bgrgba(0, 0, 0, 0.5)Default circular button background
--rk-reel-button-bg-hoverrgba(255, 255, 255, 0.1)Nav arrow background (and base hover state)
--rk-reel-button-bg-hover-strongrgba(255, 255, 255, 0.2)Nav arrow hover background
--rk-reel-button-fg#fffButton icon color
--rk-reel-button-size44pxButton width / height
--rk-reel-button-radius50%Button border-radius
--rk-reel-ui-z10Close / sound / nav z-index
--rk-reel-edge-padding16pxEdge inset for close / sound / nav arrows
--rk-reel-nav-gap8pxSpacing between stacked nav arrows
--rk-reel-transition0.2sHover transition duration
--rk-reel-loader-colorrgba(255, 255, 255, 0.12)Wave loader gradient color
--rk-reel-loader-duration1.8sWave loader animation duration
--rk-reel-error-fgrgba(255, 255, 255, 0.4)Error icon and text color
--rk-reel-slide-overlay-bglinear-gradient(transparent, rgba(0, 0, 0, 0.7))Caption scrim gradient
--rk-reel-slide-overlay-padding48px 16px 16pxCaption inner padding
--rk-reel-slide-overlay-name-color#fffAuthor name color
--rk-reel-video-bg#000Letterbox background behind <video>
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)Nested arrow background
--rk-reel-nested-button-size36pxNested arrow size
--rk-reel-timeline-trackrgba(255, 255, 255, 0.22)Track background (unplayed region)
--rk-reel-timeline-bufferedrgba(255, 255, 255, 0.4)Buffered segments color
--rk-reel-timeline-fill#fffPlayed-progress fill color
--rk-reel-timeline-cursor#fffScrub-handle pill color
--rk-reel-timeline-height3pxTrack height at rest
--rk-reel-timeline-height-active6pxTrack height on hover / focus / scrub
--rk-reel-timeline-cursor-width10pxScrub-pill width at rest
--rk-reel-timeline-cursor-width-active14pxScrub-pill width while scrubbing
--rk-reel-timeline-cursor-height24pxScrub-pill height at rest
--rk-reel-timeline-cursor-height-active32pxScrub-pill height while scrubbing
--rk-reel-timeline-transition0.15s ease-outTrack + pill grow/shrink animation

Drop the snippet below into a stylesheet loaded after @reelkit/vue-reel-player/styles.css.

css

Accessibility

The overlay root is a modal dialog (role="dialog", aria-modal="true"). Set the aria-label prop to change the screen-reader announcement; it defaults to "Video player". Each slide carries role="group", aria-roledescription="slide", and aria-label="Slide N of M".

The overlay captures focus on open and returns it to the trigger on close. Tab and Shift+Tab cycle through focusable elements inside; focus that escapes (click outside, programmatic focus) gets pulled back. Implemented with captureFocusForReturn and createFocusTrap from @reelkit/core.

Keyboard Shortcuts

KeyAction
ArrowUpPrevious slide
ArrowDownNext slide
ArrowLeftPrevious media (nested carousel)
ArrowRightNext media (nested carousel)
EscapeClose player