Vue Reel Player
Reproductor vertical de medios a pantalla completa al estilo de Instagram y TikTok para Vue 3, con @reelkit/vue-reel-player.
Características
Instalación
npm install @reelkit/vue-reel-player @reelkit/vue lucide-vue-nextImporta la hoja de estilos una vez en la entrada de tu aplicación (o en cualquier componente):
import '@reelkit/vue-reel-player/styles.css';Iconos
Los controles por defecto usan lucide-vue-next para los iconos (cerrar, sonido, flechas de navegación). Si prefieres otra librería de iconos, usa los scoped slots #controls y #navigation para poner los tuyos.
Uso básico
Renderiza una cuadrícula de miniaturas y abre el overlay en el índice pulsado. Al enlazar v-model:is-open, el ref del padre se mantiene sincronizado cuando el usuario cierra el reproductor con el botón, con un gesto o con Escape.
<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 content: ContentItem[] = [
{
id: '1',
media: [{
id: 'v1',
type: 'video',
src: '/cdn/samples/videos/video-01.mp4',
poster: '/cdn/samples/videos/video-poster-01.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Alex Johnson', avatar: '/cdn/samples/avatars/avatar-01.jpg' },
likes: 1234,
description: 'Amazing sunset vibes',
},
{
id: '2',
media: [{
id: 'img1',
type: 'image',
src: '/cdn/samples/images/image-01.jpg',
aspectRatio: 2 / 3,
}],
author: { name: 'Sarah Miller', avatar: '/cdn/samples/avatars/avatar-02.jpg' },
likes: 5678,
description: 'Nature at its finest',
},
{
id: '3',
media: [{
id: 'v2',
type: 'video',
src: '/cdn/samples/videos/video-02.mp4',
poster: '/cdn/samples/videos/video-poster-02.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Mike Chen', avatar: '/cdn/samples/avatars/avatar-03.jpg' },
likes: 3456,
description: 'Adventure awaits',
},
];
const isOpen = ref(false);
const startIndex = ref(0);
function openAt(i: number) {
startIndex.value = i;
isOpen.value = true;
}
</script>
<template>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
<button
v-for="(item, i) in content"
:key="item.id"
@click="openAt(i)"
style="aspect-ratio:9/16;cursor:pointer;overflow:hidden;padding:0;border:0"
>
<img
:src="item.media[0].poster || item.media[0].src"
style="width:100%;height:100%;object-fit:cover"
/>
</button>
</div>
<ReelPlayerOverlay
v-model:is-open="isOpen"
:content="content"
:initial-index="startIndex"
/>
</template>Scoped slots
Ocho scoped slots te permiten sustituir cualquier parte de la interfaz del reproductor. Cada uno recibe un objeto de ámbito con tipos estrictos. Los slots que no pases usan los valores por defecto.
| Slot | Ámbito | Descripción |
|---|---|---|
| #controls | { item, soundState, activeIndex, content, onClose } | Barra de controles globales propia (cerrar, sonido, compartir, etc.) |
| #error | { item, activeIndex, innerActiveIndex } | Indicador de error propio (sustituye el icono por defecto) |
| #loading | { item, activeIndex, innerActiveIndex } | Indicador de carga propio (sustituye la animación de onda por defecto) |
| #navigation | { item, activeIndex, count, onPrev, onNext } | Flechas de navegación de anterior y siguiente propias (escritorio) |
| #nestedNavigation | { media, activeIndex, count, onPrev, onNext } | Flechas propias para el slider horizontal interior |
| #nestedSlide | { item, media, index, size, isActive, isInnerActive, slideKey, defaultContent, onReady, onWaiting, onError } | Contenido propio de los slides dentro del slider horizontal interior |
| #slide | { item, index, size, isActive, slideKey, defaultContent, onReady, onWaiting, onError } | Contenido del slide totalmente propio (si se omite, se usa el de por defecto) |
| #slideOverlay | { item, index, isActive } | Overlay por slide (información del autor, me gusta, descripción, etc.) |
| #timeline | { item, activeIndex, timelineState, defaultContent } | Barra de reproducción propia. Solo se llama cuando la regla incluida (modo de timeline y duración mínima) renderizaría la barra por defecto; usa la misma lógica de auto/always/never. Usa defaultContent() para envolver el <TimelineBar /> incluido. |
<script setup lang="ts">
import { ref } from 'vue';
import {
ReelPlayerOverlay,
CloseButton,
SoundButton,
type ContentItem,
type SlideOverlaySlotScope,
type ControlsSlotScope,
} from '@reelkit/vue-reel-player';
const isOpen = ref(false);
const content = ref<ContentItem[]>([/* ... */]);
</script>
<template>
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
<!-- Custom per-slide overlay: branded caption -->
<template #slideOverlay="{ item, isActive }: SlideOverlaySlotScope">
<div v-if="isActive" style="position:absolute;bottom:80px;left:16px;color:#fff">
<div style="display:flex;align-items:center;gap:8px">
<img :src="item.author.avatar" style="width:40px;height:40px;border-radius:50%" />
<span style="font-weight:600">{{ item.author.name }}</span>
</div>
<p style="margin-top:8px">{{ item.description }}</p>
</div>
</template>
<!-- Custom global controls -->
<template #controls="{ onClose }: ControlsSlotScope">
<div style="position:absolute;top:16px;right:16px;display:flex;gap:8px">
<SoundButton />
<CloseButton :on-click="onClose" />
</div>
</template>
<!-- Custom playback timeline -->
<template #timeline="{ timelineState }: TimelineSlotScope">
<CustomTimelineBar :state="timelineState" />
</template>
</ReelPlayerOverlay>
</template>Línea de tiempo propia
Sustituye la barra de reproducción incluida por tu propia interfaz de arrastre con el slot #timeline. El slot solo se usa cuando las reglas del overlay renderizarían la barra por defecto (el mismo modo timeline y timelineMinDurationSeconds), así que no tienes que reimplementarlas. Reutiliza la clase .rk-reel-timeline en tu raíz para heredar la posición pegada al borde inferior, el relleno de la zona segura y el espacio que deja en dispositivos táctiles.
<script setup lang="ts">
import { shallowRef, onMounted, onBeforeUnmount } from 'vue';
import {
ReelPlayerOverlay,
type TimelineSlotScope,
} from '@reelkit/vue-reel-player';
import { toVueRef, type TimelineController } from '@reelkit/vue';
const trackRef = shallowRef<HTMLDivElement | null>(null);
let dispose: (() => void) | null = null;
const bind = (state: TimelineController) => {
if (trackRef.value) dispose = state.bindInteractions(trackRef.value);
};
onBeforeUnmount(() => dispose?.());
</script>
<template>
<ReelPlayerOverlay :is-open="open" :content="items" timeline="always">
<template #timeline="{ timelineState }: TimelineSlotScope">
<div class="rk-reel-timeline" style="padding: 0 16px" @vue:mounted="bind(timelineState)">
<div ref="trackRef" role="slider" style="height:6px;background:rgba(255,255,255,0.2)">
<div :style="{
width: (timelineState.progress.value * 100) + '%',
height: '100%',
background: 'linear-gradient(90deg, #6366f1, #ec4899)',
}" />
</div>
</div>
</template>
</ReelPlayerOverlay>
</template>Tipos de contenido propios
ReelPlayerOverlay es genérico respecto a la forma de tus elementos de contenido. Extiende BaseContentItem para usar cualquier modelo de datos e importa el tipo de ámbito del slot que corresponda para mantener los tipos estrictos en los slots:
<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>El mismo patrón sirve para los demás slots. Importa el tipo de ámbito correspondiente (SlideSlotScope, ControlsSlotScope, NavigationSlotScope, NestedSlideSlotScope, LoadingSlotScope) y anota la desestructuración.
Estado en la URL
Ver la demo en vivo →Crea un controlador con useOverlayUrlState de @reelkit/vue y pásalo a ReelPlayerUrlOverlay como controller: la barra de direcciones controla el reproductor, así que se abre cuando el parámetro nombra un slide y se cierra cuando el parámetro desaparece. Abrir añade una entrada al historial y cada cambio de slide la sustituye, así que recorrer un feed no añade entradas y volver una vez siempre sale. La profundidad de la URL depende de la clave del controlador: una urlIndexKey de un eje solo apunta a la publicación (?reel=3) y una urlIndexTwoAxisKey de dos ejes lleva también el índice del medio interior de una publicación con varios medios (?reel=3.2); elige una clave por aplicación, porque los dos formatos no se decodifican entre sí. Es un componente distinto de ReelPlayerOverlay, así que cada uno tiene una única forma de controlar el estado abierto: el modelo is-open o el controller de la URL, nunca los dos.
Claves incluidas
Puedes apuntar a los slides con una clave incluida: pasa con spread urlIndexKey (por posición) o urlStableIdKey (por un id estable) al controlador; las dos se reexportan desde @reelkit/vue. Consulta la guía del estado en la URL y la API del core.
Una aplicación con router debería pasar un adaptador basado en el router, para que este siga siendo la única fuente de verdad de la navegación: escribir en el historial a sus espaldas deja su ubicación desfasada y pierde el parámetro en la siguiente navegación. useVueRouterUrlAdapter de @reelkit/vue/vue-router-url-adapter es el adaptador listo para Vue Router.
<script setup lang="ts">
import { ReelPlayerUrlOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } 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="post.media[0].src" />
</RouterLink>
<ReelPlayerUrlOverlay :controller="reel" :content="props.content" />
</template>Las opciones completas de useOverlayUrlState están en la referencia de la API de Vue.
- Abrir añade una entrada al historial. Deslizar por el feed la sustituye, así que N deslizamientos no añaden entradas y volver una vez siempre sale del reproductor. Volver cierra; no cambia de slide.
- Volver solo cierra cuando el reproductor se abrió desde dentro de la aplicación, es decir, cuando el enlace añadió una entrada. Un enlace compartido abierto directamente en una pestaña nueva no tiene historial detrás, así que el botón de volver del navegador sale del sitio; el botón ✕ o Escape quitan el parámetro en el sitio y se quedan.
- Un enlace directo
?reel=3abre el reproductor en ese slide al cargar. - Un parámetro que no nombra ningún slide (un marcador desfasado, un valor editado a mano) se quita de la URL en lugar de dejar la barra de direcciones apuntando a un slide que no se puede abrir.
- La profundidad de la URL depende de la clave del controlador: un eje solo para la publicación, o dos ejes (
urlIndexTwoAxisKey) para llevar también el índice de la imagen interior de una publicación con varios medios. Elige una clave por aplicación; los formatos no se decodifican entre sí.
Una clave o dos: elige la profundidad de la URL
El mismo ReelPlayerOverlay admite las dos formas; las distingue en tiempo de ejecución por la posici ón del controlador, así que no hay ninguna prop de modo. Elige la clave al crear el controlador:
| Clave | Formato | Lleva |
|---|---|---|
urlIndexKey(…) | ?reel=3 | Solo la publicación vertical. |
urlIndexTwoAxisKey(…) | ?reel=3.2 | La publicación y el índice del medio interior de un carrusel. |
Los dos formatos son distintos a propósito: una clave de dos ejes siempre lleva punto (3.0, nunca un 3 suelto), así que un enlace de un eje no se decodifica como de dos. Por eso cambiar una aplicación de una clave a otra invalida los enlaces compartidos antes. Elige una forma y mantenla.
import { useOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/vue';
const reel = useOverlayUrlState({
param: 'reel',
...urlIndexTwoAxisKey({
outerCount: () => content.value.length,
innerCounts: () => content.value.map((post) => post.media.length),
}),
});
// A link now names both axes: post 3, inner media 2 — ?reel=3.2Enlaces estables. El índice es posicional, así que un ?reel=3 guardado abre otra publicación en cuanto el feed se reordena, y en un feed eso es lo normal, no la excepción. urlStableIdKey usa como clave el id estable de cada publicación y recorre el feed actual; una sola llamada cubre el caso habitual.
const reel = useOverlayUrlState({
param: 'reel',
...urlStableIdKey({ items: () => content.value }),
});Pasa hashCodec: base64UrlCodec para codificar el id en base64url en la URL: es una ofuscación reversible, no un hash criptográfico.
Si usas otro campo como clave (un slug) o paginas un feed infinito con locateAsync, crea tú el codec y el locator. Son dos tareas distintas: codec escribe la identidad en la URL y locator encuentra dónde está esa identidad.
const reel = useOverlayUrlState({
param: 'reel',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => content.value.findIndex((x) => x.id === id),
identify: (index) => content.value[index].id,
},
});Feeds infinitos. locate es síncrono, así que solo puede responder por las publicaciones ya cargadas: un enlace compartido a la publicación 400 de un feed que ha cargado 20 no encuentra nada. locateAsync es la alternativa y solo se llama cuando locate falla.
Atajo
¿Usas el id del elemento como clave? Sáltate el codec y el locator hechos a mano y pasa locateAsync directamente a urlStableIdKey({ items, locateAsync }) (carga cuando no lo encuentra y después devuelve el índice). La versión más completa de abajo es para usar otro campo como clave o para tener el control total.
const reel = useOverlayUrlState({
param: 'reel',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => content.value.findIndex((x) => x.id === id),
identify: (index) => content.value[index].id,
locateAsync: async (id) => {
const loaded = await loadById(id); // or loadUntil(id) — fetch just that one, or page up to it
if (!loaded) return null; // exhausted — link names no post
content.value = loaded; // commit — the overlay renders from this state
return loaded.findIndex((x) => x.id === id); // wherever it landed
},
},
});- Mientras
locateAsyncestá pendiente, el reproductor sigue cerrado y el parámetro no se toca, así que el enlace directo sobrevive a la carga.nullo un rechazo quitan el parámetro. - Una respuesta que llega después de que la URL haya cambiado, después de cerrar o después de desmontar se descarta: una carga lenta no puede abrir un slide que nadie ha pedido.
- Mientras está pendiente no se renderiza nada; la página ya es dueña de ese estado de carga, así que renderiza tu propio esqueleto.
- No hay tiempo límite: el reproductor no puede saber lo largo que es el feed. Resuelve con
nullcuando se agote la paginación, o el overlay seguirá cerrado indefinidamente.
Referencia de la API
Props de ReelPlayerOverlay
ReelPlayerOverlayProps
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
ariaLabel | string | 'Video player' | Etiqueta accesible de la región del diálogo; los lectores de pantalla la anuncian al abrirse el overlay |
aspectRatio | number | 9 / 16 | Relación entre ancho y alto del contenedor en escritorio. En móvil se usa todo el viewport. |
content | T[] (extends BaseContentItem) | required | Array de elementos de contenido que mostrar en el reproductor |
enableNavKeys | boolean | true | Activa la navegación con las teclas de flecha |
enableWheel | boolean | true | Activa la navegación con la rueda del ratón |
initialIndex | number | 0 | Índice, empezando en cero, del elemento visible al principio |
initialInnerIndex | number | 0 | Índice del medio interior con el que abrir, solo para la publicación visible al principio: permite que una URL de dos ejes enlace a una imagen concreta de una publicación con varios medios. Se ignora cuando el usuario navega. |
isOpen | boolean | required | Controla la visibilidad del overlay; con false el overlay se quita del DOM |
loop | boolean | false | Activa el bucle infinito entre slides |
swipeDistanceFactor | number | 0.12 | Fracción mínima de la distancia de deslizamiento para cambiar de slide |
timeline | 'auto' | 'always' | 'never' | 'auto' | Cuándo se muestra la barra de reproducción incluida. 'auto' solo la renderiza con vídeos más largos que timelineMinDurationSeconds; 'always' la renderiza siempre que el slide activo tenga un vídeo; 'never' desactiva la barra incluida (usa el slot #timeline para sustituirla por completo). |
timelineMinDurationSeconds | number | 30 | Duración mínima del vídeo (en segundos) para que timeline='auto' renderice la barra incluida. Los clips cortos en bucle por debajo de este umbral no la muestran. |
transitionDuration | number | 300 | Duración de la animación de los slides en ms |
wheelDebounceMs | number | 200 | Duración del debounce de los eventos de la rueda en ms |
Props de ReelPlayerUrlOverlay
ReelPlayerUrlOverlayProps
Acepta todas las props de arriba salvo is-open, que se sustituye por controller. initial-index se ignora: la posición del controlador elige el slide, así que un valor pasado junto a él se sobrescribe en cada apertura.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
controller | UrlStateController | required | Controlador de useOverlayUrlState. Su position decide si el overlay está abierto y qué slide muestra; el overlay escribe a través de él al cambiar de slide y al cerrar. |
Eventos
| Evento | Datos | Descripción |
|---|---|---|
| @api-ready | ReelPlayerApi | Se emite cuando el slider está listo y expone la API imperativa |
| @close | void | Se emite al cerrar el reproductor |
| @slide-change | number | Se emite con el índice del nuevo slide activo después de un cambio |
| @inner-slide-change | outer: number, inner: number | Se emite cuando cambia el índice del medio interior de la publicación activa: al navegar por dentro y al activar una publicación (su índice interior actual, 0 con un solo medio). |
| @update:is-open | boolean | Se emite al cerrar; permite usar `v-model:is-open` |
v-model:is-open
Usa v-model:is-open para controlar el overlay con un único enlace. El patrón anterior de :is-open + @close sigue funcionando si necesitas el evento explícito.
<template>
<button @click="open = true">Open</button>
<ReelPlayerOverlay v-model:is-open="open" :content="content" />
</template>Tipos
ContentItem
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único |
media | MediaItem[] | Uno o varios medios (imagen o vídeo) |
author | { name: string; avatar?: string } | Autor que se muestra en el overlay del slide por defecto |
description | string? | Texto del pie |
likes | number? | Número de me gusta |
TimelineBarProps
interface TimelineBarProps {
class?: string;
style?: CSSProperties;
}TimelineSlotScope<T>
interface TimelineSlotScope<T extends BaseContentItem> {
item: T;
activeIndex: number;
timelineState: TimelineController;
defaultContent: () => VNode | VNode[];
}MediaItem
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único |
type | 'image' | 'video' | Tipo de medio |
src | string | URL del medio |
poster | string? | URL de la miniatura del póster en los vídeos |
aspectRatio | number | Relación entre ancho y alto. Valores < 1 = vertical (cover), ≥ 1 = horizontal (contain). |
Subcomponentes
Úsalos en tus plantillas propias de #controls, #slide o #slideOverlay. Pasa las dimensiones y los callbacks desde el ámbito del slot para que sigan funcionando la reproducción automática, la captura del póster y la sincronización del sonido.
CloseButton
Botón circular de cerrar independiente con el estilo por defecto del reproductor. Úsalo dentro de #controls.
<script setup lang="ts">
import { CloseButton } from '@reelkit/vue-reel-player';
</script>
<template>
<CloseButton :on-click="onClose" />
<CloseButton :on-click="onClose" class-name="my-close-btn" :style="{ top: '24px', right: '24px' }" />
</template>SoundButton
Botón de silencio. Renderízalo dentro de un SoundProvider (ReelPlayerOverlay ya incluye uno). Se oculta cuando el slide activo no tiene vídeo.
<script setup lang="ts">
import { SoundButton } from '@reelkit/vue-reel-player';
</script>
<template>
<SoundButton />
<SoundButton disabled class-name="my-sound-btn" />
</template>TimelineBar
Barra de reproducción por defecto. Lee del TimelineProvider más cercano (se monta automáticamente dentro de ReelPlayerOverlay) y renderiza la pista, los rangos cargados, el relleno del progreso y la píldora de arrastre. Cambia su tema con las propiedades personalizadas --rk-reel-timeline-* o sustitúyela con el slot #timeline.
<script setup lang="ts">
import type { TimelineSlotScope } from '@reelkit/vue-reel-player';
</script>
<template>
<!-- Wrap or augment the default bar from #timeline: -->
<ReelPlayerOverlay>
<template #timeline="{ defaultContent }: TimelineSlotScope">
<MyTimecode />
<component :is="defaultContent" />
</template>
</ReelPlayerOverlay>
</template>SlideOverlay
El overlay con degradado por defecto que muestra el autor, la descripción y los me gusta. Se renderiza cuando el contenido tiene esos campos. Sustitúyelo u ocúltalo con el slot #slideOverlay.
<script setup lang="ts">
import { SlideOverlay } from '@reelkit/vue-reel-player';
</script>
<template>
<SlideOverlay
:author="{ name: 'John', avatar: '/avatar.jpg' }"
description="Amazing content"
:likes="12500"
/>
</template>ImageSlide
Slide de imagen con carga diferida y object-fit: cover por defecto. Combínalo dentro del slot #slide para personalizar cómo se renderiza la imagen sin perder el comportamiento incluido.
<script setup lang="ts">
import { ImageSlide } from '@reelkit/vue-reel-player';
</script>
<template>
<ImageSlide :src="media.src" :size="size" />
<ImageSlide
:src="media.src"
:size="size"
class-name="my-image-slide"
:style="{ backgroundColor: '#1a1a1a', borderRadius: '12px' }"
:img-style="{ objectFit: 'contain' }"
/>
</template>VideoSlide
Slide de vídeo basado en un elemento <video> compartido. Evita que el sonido se corte en iOS y gestiona los fotogramas de póster y la memoria de la posición. Renderízalo dentro de un SoundProvider (ReelPlayerOverlay ya incluye uno).
<script setup lang="ts">
import { VideoSlide } from '@reelkit/vue-reel-player';
</script>
<template>
<VideoSlide
:src="media.src"
:poster="media.poster"
:aspect-ratio="9 / 16"
:size="size"
:is-active="isActive"
:slide-key="slideKey"
:style="{ borderRadius: '12px' }"
/>
</template>Crear slides propios
Usa #slide con ImageSlide / VideoSlide para personalizar cómo se renderizan los medios sin perder el comportamiento incluido (reproducción automática, captura del póster, sincronización del sonido).
<script setup lang="ts">
import {
ReelPlayerOverlay,
ImageSlide,
VideoSlide,
} from '@reelkit/vue-reel-player';
</script>
<template>
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
<template #slide="{ item, size, isActive, slideKey }">
<ImageSlide
v-if="item.media[0].type === 'image'"
:src="item.media[0].src"
:size="size"
:style="{ backgroundColor: '#111' }"
:img-style="{ objectFit: 'contain' }"
/>
<VideoSlide
v-else
:src="item.media[0].src"
:poster="item.media[0].poster"
:aspect-ratio="item.media[0].aspectRatio"
:size="size"
:is-active="isActive"
:slide-key="slideKey"
:style="{ borderRadius: '16px' }"
/>
</template>
</ReelPlayerOverlay>
</template>Carga de contenido y gestión de errores
El reproductor sigue el estado de carga y de error de cada slide. Mientras el contenido se carga se muestra una animación de onda; con un medio roto aparece un icono de error. El reproductor guarda en caché las URLs que fallan, así que al volver a abrir un slide roto no se reintenta.
Callbacks del ciclo de vida
Si usas el slot #slide, llama a estos callbacks desde el ámbito del slot para controlar el indicador de carga:
| Callback | Cuándo llamarlo |
|---|---|
onReady | La imagen se ha cargado o el vídeo ha empezado a reproducirse. Limpia los estados de carga y de error. |
onWaiting | El vídeo está cargando a mitad de la reproducción. Muestra el indicador de carga. |
onError | El contenido no se ha podido cargar. Muestra el overlay de error y guarda la URL en caché como rota. |
<!-- Inside #slide — wire callbacks to your custom media -->
<template #slide="{ item, size, isActive, onReady, onWaiting, onError }">
<div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
<img
v-if="item.media[0].type === 'image'"
:src="item.media[0].src"
@load="onReady"
@error="onError"
style="width:100%;height:100%;object-fit:cover"
/>
<video
v-else
:src="item.media[0].src"
:autoplay="isActive"
@canplay="onReady"
@waiting="onWaiting"
@error="onError"
style="width:100%;height:100%;object-fit:cover"
/>
</div>
</template>Interfaz de carga y de error propia
Sustituye la animación de onda y el icono de error por defecto con los slots #loading y #error:
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
<template #loading="{ activeIndex }">
<div
style="position:absolute;inset:0;z-index:10;display:flex;
align-items:center;justify-content:center;color:#fff;font-size:14px"
>
Loading slide {{ activeIndex + 1 }}...
</div>
</template>
<template #error="{ activeIndex }">
<div
style="position:absolute;inset:0;z-index:10;display:flex;
flex-direction:column;align-items:center;justify-content:center;
gap:12px;color:rgba(255,255,255,0.5)"
>
<span style="font-size:48px">!</span>
<span>Slide {{ activeIndex + 1 }} failed to load</span>
</div>
</template>
</ReelPlayerOverlay>Línea de tiempo
El overlay renderiza una barra de reproducción incluida sobre el vídeo activo. Controla cuándo con la prop timeline: 'auto' (por defecto) la renderiza siempre que el medio activo sea un vídeo más largo que timelineMinDurationSeconds (30 por defecto), 'always' siempre que haya un vídeo activo y 'never' la desactiva. Para una barra de arrastre totalmente propia, usa el slot #timeline; su ámbito expone un timelineState respaldado por el TimelineController que hay debajo.
<ReelPlayerOverlay
:is-open="isOpen"
:content="items"
timeline="auto"
:timeline-min-duration-seconds="30"
@close="isOpen = false"
/>Cambia su tema con las propiedades personalizadas de CSS --rk-reel-timeline-*.
Contexto de sonido
ReelPlayerOverlay monta un SoundProvider en su raíz, así que cualquier componente renderizado dentro puede leer o cambiar el estado de silencio con useSoundState. El composable se reexporta desde @reelkit/vue-reel-player, así que no necesitas importar @reelkit/vue por separado.
<script setup lang="ts">
import { useSoundState, toVueRef } from '@reelkit/vue';
// Inside a custom control rendered from the #controls slot:
const soundState = useSoundState();
const muted = toVueRef(soundState.muted);
</script>
<template>
<button @click="soundState.toggle()">
{{ muted ? 'Unmute' : 'Mute' }}
</button>
</template>Dentro del reproductor, el slot #controls también expone soundState en su ámbito. Es mejor usarlo cuando solo lo necesitas dentro de la plantilla de los controles.
Clases CSS
Las clases CSS son normales (no scoped). Una hoja de estilos cargada después de @reelkit/vue-reel-player/styles.css puede sobrescribir cualquiera de ellas con un selector más específico. Para cambiar colores, tamaños y z-index, usa las propiedades personalizadas de CSS de la sección Temas de más abajo.
| Clase | Componente | Descripción |
|---|---|---|
.rk-reel-overlay | Overlay | Fondo fijo a pantalla completa (fondo, z-index) |
.rk-reel-container | Overlay | Contenedor del reproductor (posición, desbordamiento) |
.rk-reel-loader | Overlay | Overlay con la animación de carga en onda |
.rk-reel-media-error | Overlay | Overlay del estado de error (icono y texto centrados) |
.rk-reel-media-error-text | Overlay | Texto del mensaje de error |
.rk-reel-button | Controls | Botón circular de icono compartido (cerrar, sonido, flechas de navegación) |
.rk-reel-close-btn | Controls | Botón de cerrar |
.rk-reel-sound-btn | Controls | Botón de sonido |
.rk-reel-nav-arrows | Navigation | Contenedor de flechas solo para escritorio (oculto por debajo de 768px) |
.rk-reel-nav-button | Navigation | Cada flecha de navegación de anterior o siguiente |
.rk-reel-slide-wrapper | Slide | Envoltorio alrededor del medio y el overlay |
.rk-reel-slide-overlay | SlideOverlay | Contenedor del overlay con degradado |
.rk-reel-slide-overlay-author | SlideOverlay | Fila del autor (avatar y nombre) |
.rk-reel-slide-overlay-avatar | SlideOverlay | Imagen del avatar del autor |
.rk-reel-slide-overlay-name | SlideOverlay | Texto con el nombre del autor |
.rk-reel-slide-overlay-description | SlideOverlay | Texto de la descripción |
.rk-reel-slide-overlay-likes | SlideOverlay | Fila de me gusta (corazón y recuento) |
.rk-reel-video-container | VideoSlide | Envoltorio del vídeo (fondo, desbordamiento) |
.rk-reel-video-element | VideoSlide | El elemento <video> |
.rk-reel-video-poster | VideoSlide | Imagen del póster (se desvanece al reproducir) |
.rk-reel-video-poster.rk-visible | VideoSlide | Modificador de estado que se aplica al póster mientras el vídeo está en pausa o cargando |
.rk-reel-nested-indicator | NestedSlider | Paginación con puntos bajo los slides con varios medios (su posición cambia entre escritorio y táctil) |
.rk-reel-nested-nav | NestedSlider | Flechas del carrusel horizontal (ocultas por debajo de 768px) |
.rk-reel-nested-nav-next | NestedSlider | Posición de la flecha anidada de siguiente |
.rk-reel-nested-nav-prev | NestedSlider | Posición de la flecha anidada de anterior |
.rk-reel-timeline | TimelineBar | Envoltorio de la barra de arrastre. Reutilízalo en las raíces de un slot `#timeline` propio para heredar la posición pegada al borde inferior, el relleno de la zona segura y el espacio que deja al overlay del slide en dispositivos táctiles. |
.rk-reel-timeline-track | TimelineBar | Pista (la zona sin reproducir) |
.rk-reel-timeline-buffered | TimelineBar | Capa de segmentos cargados |
.rk-reel-timeline-fill | TimelineBar | Relleno del progreso reproducido |
.rk-reel-timeline-cursor | TimelineBar | Píldora de arrastre (flota sobre la pista) |
Temas
Cada color, tamaño, z-index y transición vive en una propiedad personalizada de CSS. Sobrescribe una o varias en :root (o en cualquier ancestro del overlay) para cambiar el tema sin tocar el código de los componentes. Los tokens coinciden con los de @reelkit/react-reel-player, así que los cambios sirven para los dos bindings.
| Token | Por defecto | Controla |
|---|---|---|
--rk-reel-overlay-bg | #000 | Color del fondo a pantalla completa |
--rk-reel-overlay-z | 1000 | z-index del overlay |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Fondo por defecto de los botones circulares |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Fondo de las flechas de navegación (y estado hover base) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Fondo de las flechas de navegación al pasar el ratón |
--rk-reel-button-fg | #fff | Color del icono de los botones |
--rk-reel-button-size | 44px | Ancho / alto de los botones |
--rk-reel-button-radius | 50% | border-radius de los botones |
--rk-reel-ui-z | 10 | z-index de cerrar / sonido / navegación |
--rk-reel-edge-padding | 16px | Separación del borde de cerrar / sonido / flechas |
--rk-reel-nav-gap | 8px | Espacio entre las flechas de navegación apiladas |
--rk-reel-transition | 0.2s | Duración de la transición al pasar el ratón |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Color del degradado de la animación de onda |
--rk-reel-loader-duration | 1.8s | Duración de la animación de onda |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Color del icono y el texto de error |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Degradado del fondo del pie |
--rk-reel-slide-overlay-padding | 48px 16px 16px | Relleno interior del pie |
--rk-reel-slide-overlay-name-color | #fff | Color del nombre del autor |
--rk-reel-video-bg | #000 | Fondo de las bandas detrás del <video> |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Fondo de las flechas anidadas |
--rk-reel-nested-button-size | 36px | Tamaño de las flechas anidadas |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Fondo de la pista (la zona sin reproducir) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Color de los segmentos cargados |
--rk-reel-timeline-fill | #fff | Color del relleno del progreso reproducido |
--rk-reel-timeline-cursor | #fff | Color de la píldora de arrastre |
--rk-reel-timeline-height | 3px | Altura de la pista en reposo |
--rk-reel-timeline-height-active | 6px | Altura de la pista al pasar el ratón, con foco o al arrastrar |
--rk-reel-timeline-cursor-width | 10px | Ancho de la píldora en reposo |
--rk-reel-timeline-cursor-width-active | 14px | Ancho de la píldora al arrastrar |
--rk-reel-timeline-cursor-height | 24px | Altura de la píldora en reposo |
--rk-reel-timeline-cursor-height-active | 32px | Altura de la píldora al arrastrar |
--rk-reel-timeline-transition | 0.15s ease-out | Animación al crecer y encoger la pista y la píldora |
Pega el fragmento de abajo en una hoja de estilos cargada después de @reelkit/vue-reel-player/styles.css.
/* Brand the reel-player overlay */
:root {
--rk-reel-overlay-bg: #0f172a;
--rk-reel-button-bg: rgba(99, 102, 241, 0.65);
--rk-reel-button-bg-hover-strong: rgba(168, 85, 247, 0.85);
--rk-reel-edge-padding: 24px;
--rk-reel-button-size: 52px;
/* Timeline bar: brand-matched, beefier on desktop */
--rk-reel-timeline-track: rgba(99, 102, 241, 0.25);
--rk-reel-timeline-buffered: rgba(168, 85, 247, 0.45);
--rk-reel-timeline-fill: #a855f7;
--rk-reel-timeline-cursor: #a855f7;
--rk-reel-timeline-height: 4px;
--rk-reel-timeline-height-active: 8px;
--rk-reel-timeline-cursor-width-active: 18px;
--rk-reel-timeline-transition: 0.2s ease-out;
}Accesibilidad
La raíz del overlay es un diálogo modal (role="dialog", aria-modal="true"). Usa la prop aria-label para cambiar lo que anuncia el lector de pantalla; por defecto es "Video player". Cada slide lleva role="group", aria-roledescription="slide" y aria-label="Slide N of M".
El overlay captura el foco al abrirse y lo devuelve al disparador al cerrarse. Tab y Mayús+Tab recorren los elementos enfocables de dentro; el foco que sale (un clic fuera, un foco por código) vuelve dentro. Está implementado con captureFocusForReturn y createFocusTrap de @reelkit/core.
Atajos de teclado
| Tecla | Acción |
|---|---|
ArrowUp | Slide anterior |
ArrowDown | Slide siguiente |
ArrowLeft | Medio anterior (carrusel anidado) |
ArrowRight | Medio siguiente (carrusel anidado) |
Escape | Cierra el reproductor |