Vue Reel Player
Full-screen Instagram/TikTok-style vertical media player for Vue 3, built on @reelkit/vue-reel-player.
Features
Installation
Import the stylesheet once in your app entry (or any component):
Icons
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.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| ariaLabel | string | 'Video player' | Accessible label for the dialog region; announced by screen readers when the overlay opens |
| aspectRatio | number | 9 / 16 | Width/height ratio for the desktop container. Mobile uses the full viewport. |
| content | T[] (extends BaseContentItem) | required | Array of content items to display in the player |
| enableNavKeys | boolean | true | Enable keyboard arrow-key navigation |
| enableWheel | boolean | true | Enable mouse-wheel navigation |
| initialIndex | number | 0 | Zero-based index of the initially visible item |
| isOpen | boolean | required | Controls overlay visibility; when false the overlay is removed from the DOM |
| loop | boolean | false | Enable infinite loop between slides |
| swipeDistanceFactor | number | 0.12 | Minimum 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). |
| timelineMinDurationSeconds | number | 30 | Minimum video duration (seconds) for timeline='auto' to render the built-in bar. Short looping clips below this threshold are suppressed. |
| transitionDuration | number | 300 | Slide animation duration in ms |
| wheelDebounceMs | number | 200 | Debounce duration for wheel events in ms |
Events
| Event | Payload | Description |
|---|---|---|
| @api-ready | ReelApi | Emitted once the slider is ready, exposing the imperative API |
| @close | void | Emitted when the player closes |
| @slide-change | number | Emitted with the new active slide index after a change |
| @update:is-open | boolean | Emitted 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.
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.
| Slot | Scope | Description |
|---|---|---|
| #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 />. |
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.
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:
The same pattern works for every other slot. Import the matching scope type (SlideSlotScope, ControlsSlotScope, NavigationSlotScope, NestedSlideSlotScope, LoadingSlotScope) and annotate the destructure.
Types
ContentItem
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| media | MediaItem[] | One or more media assets (image or video) |
| author | { name: string; avatar?: string } | Author shown in the default slide overlay |
| description | string? | Caption text |
| likes | number? | Likes count |
TimelineBarProps
TimelineSlotScope<T>
MediaItem
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| type | 'image' | 'video' | Media type |
| src | string | URL of the media asset |
| poster | string? | Poster thumbnail URL for video items |
| aspectRatio | number | width/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.
SoundButton
Mute/unmute toggle. Render it inside a SoundProvider (ReelPlayerOverlay provides one). Hidden when the active slide has no video.
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.
SlideOverlay
The default gradient overlay showing author, description, and likes. Renders when content carries those fields. Replace or hide it via the #slideOverlay slot.
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.
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).
Composing custom slides
#slide with ImageSlide / VideoSlide to customize media rendering while keeping all built-in behavior (autoplay, poster capture, sound sync).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:
| Callback | When to call |
|---|---|
| onReady | Image loaded or video started playing. Clears loading and error states. |
| onWaiting | Video is buffering mid-playback. Shows the loading indicator. |
| onError | Content failed to load. Shows error overlay and caches the URL as broken. |
Custom Loading & Error UI
Replace the default wave loader and error icon via the #loading and #error slots:
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.
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.
#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.
| Class | Component | Description |
|---|---|---|
| .rk-reel-overlay | Overlay | Fixed full-screen backdrop (background, z-index) |
| .rk-reel-container | Overlay | Player container (position, overflow) |
| .rk-reel-loader | Overlay | Wave loading animation overlay |
| .rk-reel-media-error | Overlay | Error state overlay (centered icon + text) |
| .rk-reel-media-error-text | Overlay | Error message text |
| .rk-reel-button | Controls | Shared circular icon button (close, sound, nav arrows) |
| .rk-reel-close-btn | Controls | Close button |
| .rk-reel-sound-btn | Controls | Sound toggle button |
| .rk-reel-nav-arrows | Navigation | Desktop-only arrow container (hidden below 768px) |
| .rk-reel-nav-button | Navigation | Individual prev/next nav arrow |
| .rk-reel-slide-wrapper | Slide | Wrapper around media + overlay |
| .rk-reel-slide-overlay | SlideOverlay | Gradient overlay container |
| .rk-reel-slide-overlay-author | SlideOverlay | Author row (avatar + name) |
| .rk-reel-slide-overlay-avatar | SlideOverlay | Author avatar image |
| .rk-reel-slide-overlay-name | SlideOverlay | Author name text |
| .rk-reel-slide-overlay-description | SlideOverlay | Description text |
| .rk-reel-slide-overlay-likes | SlideOverlay | Likes row (heart + count) |
| .rk-reel-video-container | VideoSlide | Video wrapper (background, overflow) |
| .rk-reel-video-element | VideoSlide | The <video> element |
| .rk-reel-video-poster | VideoSlide | Poster image (fades out on play) |
| .rk-reel-video-poster.rk-visible | VideoSlide | State modifier applied to the poster while the video is paused/loading |
| .rk-reel-nested-indicator | NestedSlider | Dot pagination under multi-media slides (position varies desktop vs. touch) |
| .rk-reel-nested-nav | NestedSlider | Horizontal carousel arrows (hidden below 768px) |
| .rk-reel-nested-nav-next | NestedSlider | Nested next arrow position |
| .rk-reel-nested-nav-prev | NestedSlider | Nested prev arrow position |
| .rk-reel-timeline | TimelineBar | Scrub-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-track | TimelineBar | Track (unplayed region) |
| .rk-reel-timeline-buffered | TimelineBar | Buffered segments layer |
| .rk-reel-timeline-fill | TimelineBar | Played-progress fill |
| .rk-reel-timeline-cursor | TimelineBar | Scrub-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.
| Token | Default | Controls |
|---|---|---|
| --rk-reel-overlay-bg | #000 | Full-screen backdrop color |
| --rk-reel-overlay-z | 1000 | Overlay z-index |
| --rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Default circular button background |
| --rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Nav arrow background (and base hover state) |
| --rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Nav arrow hover background |
| --rk-reel-button-fg | #fff | Button icon color |
| --rk-reel-button-size | 44px | Button width / height |
| --rk-reel-button-radius | 50% | Button border-radius |
| --rk-reel-ui-z | 10 | Close / sound / nav z-index |
| --rk-reel-edge-padding | 16px | Edge inset for close / sound / nav arrows |
| --rk-reel-nav-gap | 8px | Spacing between stacked nav arrows |
| --rk-reel-transition | 0.2s | Hover transition duration |
| --rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Wave loader gradient color |
| --rk-reel-loader-duration | 1.8s | Wave loader animation duration |
| --rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Error icon and text color |
| --rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Caption scrim gradient |
| --rk-reel-slide-overlay-padding | 48px 16px 16px | Caption inner padding |
| --rk-reel-slide-overlay-name-color | #fff | Author name color |
| --rk-reel-video-bg | #000 | Letterbox background behind <video> |
| --rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Nested arrow background |
| --rk-reel-nested-button-size | 36px | Nested arrow size |
| --rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Track background (unplayed region) |
| --rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Buffered segments color |
| --rk-reel-timeline-fill | #fff | Played-progress fill color |
| --rk-reel-timeline-cursor | #fff | Scrub-handle pill color |
| --rk-reel-timeline-height | 3px | Track height at rest |
| --rk-reel-timeline-height-active | 6px | Track height on hover / focus / scrub |
| --rk-reel-timeline-cursor-width | 10px | Scrub-pill width at rest |
| --rk-reel-timeline-cursor-width-active | 14px | Scrub-pill width while scrubbing |
| --rk-reel-timeline-cursor-height | 24px | Scrub-pill height at rest |
| --rk-reel-timeline-cursor-height-active | 32px | Scrub-pill height while scrubbing |
| --rk-reel-timeline-transition | 0.15s ease-out | Track + pill grow/shrink animation |
Drop the snippet below into a stylesheet loaded after @reelkit/vue-reel-player/styles.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
| Key | Action |
|---|---|
| ArrowUp | Previous slide |
| ArrowDown | Next slide |
| ArrowLeft | Previous media (nested carousel) |
| ArrowRight | Next media (nested carousel) |
| Escape | Close player |