Vue Reel Player

Reproductor vertical de medios a pantalla completa al estilo de Instagram y TikTok para Vue 3, con @reelkit/vue-reel-player.

Ver la demo en vivo →

Características

Deslizamiento vertical
Táctil, arrastre, teclado, rueda
Reproducción automática
Se reproduce al hacerse visible
Botón de sonido
Sin cortes en iOS
Varios medios
Carruseles horizontales anidados
Recuerda la posición
Continúa donde lo dejaste
Captura de fotogramas
Fundido del póster al vídeo
Virtualizado
Solo 3 slides en el DOM
Relación de aspecto
9:16 en escritorio, completo en móvil
Navegación en escritorio
Botones de flecha
Tipos genéricos
Modelos de datos de contenido propios
Scoped slots
Personaliza cada elemento de la interfaz
v-model:is-open
Enlace bidireccional de la visibilidad
Estado en la URL
Enlaces compartibles y cierre con el botón de volver

Instalación

bash
npm install @reelkit/vue-reel-player @reelkit/vue lucide-vue-next

Importa la hoja de estilos una vez en la entrada de tu aplicación (o en cualquier componente):

typescript
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.

App.vue
<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ÁmbitoDescripció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.
vue
<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.

vue
<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:

vue
<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.

vue
<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=3 abre 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:

ClaveFormatoLleva
urlIndexKey(…)?reel=3Solo la publicación vertical.
urlIndexTwoAxisKey(…)?reel=3.2La 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.

typescript
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.2

Enlaces 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.

typescript
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.

typescript
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.

typescript
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 locateAsync está pendiente, el reproductor sigue cerrado y el parámetro no se toca, así que el enlace directo sobrevive a la carga. null o 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 null cuando se agote la paginación, o el overlay seguirá cerrado indefinidamente.

Referencia de la API

Props de ReelPlayerOverlay

ReelPlayerOverlayProps

PropTipoPor defectoDescripción
ariaLabelstring'Video player'Etiqueta accesible de la región del diálogo; los lectores de pantalla la anuncian al abrirse el overlay
aspectRationumber9 / 16Relación entre ancho y alto del contenedor en escritorio. En móvil se usa todo el viewport.
contentT[] (extends BaseContentItem)requiredArray de elementos de contenido que mostrar en el reproductor
enableNavKeysbooleantrueActiva la navegación con las teclas de flecha
enableWheelbooleantrueActiva la navegación con la rueda del ratón
initialIndexnumber0Índice, empezando en cero, del elemento visible al principio
initialInnerIndexnumber0Í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.
isOpenbooleanrequiredControla la visibilidad del overlay; con false el overlay se quita del DOM
loopbooleanfalseActiva el bucle infinito entre slides
swipeDistanceFactornumber0.12Fracció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).
timelineMinDurationSecondsnumber30Duració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.
transitionDurationnumber300Duración de la animación de los slides en ms
wheelDebounceMsnumber200Duració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.

PropTipoPor defectoDescripción
controllerUrlStateControllerrequiredControlador 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

EventoDatosDescripción
@api-readyReelPlayerApiSe emite cuando el slider está listo y expone la API imperativa
@closevoidSe emite al cerrar el reproductor
@slide-changenumberSe emite con el índice del nuevo slide activo después de un cambio
@inner-slide-changeouter: number, inner: numberSe 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-openbooleanSe 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.

vue
<template>
  <button @click="open = true">Open</button>
  <ReelPlayerOverlay v-model:is-open="open" :content="content" />
</template>

Tipos

ContentItem

CampoTipoDescripción
idstringIdentificador único
mediaMediaItem[]Uno o varios medios (imagen o vídeo)
author{ name: string; avatar?: string }Autor que se muestra en el overlay del slide por defecto
descriptionstring?Texto del pie
likesnumber?Número de me gusta

TimelineBarProps

typescript
interface TimelineBarProps {
  class?: string;
  style?: CSSProperties;
}

TimelineSlotScope<T>

typescript
interface TimelineSlotScope<T extends BaseContentItem> {
  item: T;
  activeIndex: number;
  timelineState: TimelineController;
  defaultContent: () => VNode | VNode[];
}

MediaItem

CampoTipoDescripción
idstringIdentificador único
type'image' | 'video'Tipo de medio
srcstringURL del medio
posterstring?URL de la miniatura del póster en los vídeos
aspectRationumberRelació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.

vue
<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.

vue
<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.

vue
<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.

vue
<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.

vue
<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).

vue
<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).

vue
<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:

CallbackCuándo llamarlo
onReadyLa imagen se ha cargado o el vídeo ha empezado a reproducirse. Limpia los estados de carga y de error.
onWaitingEl vídeo está cargando a mitad de la reproducción. Muestra el indicador de carga.
onErrorEl contenido no se ha podido cargar. Muestra el overlay de error y guarda la URL en caché como rota.
vue
<!-- 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:

vue
<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.

vue
<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.

vue
<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.

ClaseComponenteDescripción
.rk-reel-overlayOverlayFondo fijo a pantalla completa (fondo, z-index)
.rk-reel-containerOverlayContenedor del reproductor (posición, desbordamiento)
.rk-reel-loaderOverlayOverlay con la animación de carga en onda
.rk-reel-media-errorOverlayOverlay del estado de error (icono y texto centrados)
.rk-reel-media-error-textOverlayTexto del mensaje de error
.rk-reel-buttonControlsBotón circular de icono compartido (cerrar, sonido, flechas de navegación)
.rk-reel-close-btnControlsBotón de cerrar
.rk-reel-sound-btnControlsBotón de sonido
.rk-reel-nav-arrowsNavigationContenedor de flechas solo para escritorio (oculto por debajo de 768px)
.rk-reel-nav-buttonNavigationCada flecha de navegación de anterior o siguiente
.rk-reel-slide-wrapperSlideEnvoltorio alrededor del medio y el overlay
.rk-reel-slide-overlaySlideOverlayContenedor del overlay con degradado
.rk-reel-slide-overlay-authorSlideOverlayFila del autor (avatar y nombre)
.rk-reel-slide-overlay-avatarSlideOverlayImagen del avatar del autor
.rk-reel-slide-overlay-nameSlideOverlayTexto con el nombre del autor
.rk-reel-slide-overlay-descriptionSlideOverlayTexto de la descripción
.rk-reel-slide-overlay-likesSlideOverlayFila de me gusta (corazón y recuento)
.rk-reel-video-containerVideoSlideEnvoltorio del vídeo (fondo, desbordamiento)
.rk-reel-video-elementVideoSlideEl elemento <video>
.rk-reel-video-posterVideoSlideImagen del póster (se desvanece al reproducir)
.rk-reel-video-poster.rk-visibleVideoSlideModificador de estado que se aplica al póster mientras el vídeo está en pausa o cargando
.rk-reel-nested-indicatorNestedSliderPaginación con puntos bajo los slides con varios medios (su posición cambia entre escritorio y táctil)
.rk-reel-nested-navNestedSliderFlechas del carrusel horizontal (ocultas por debajo de 768px)
.rk-reel-nested-nav-nextNestedSliderPosición de la flecha anidada de siguiente
.rk-reel-nested-nav-prevNestedSliderPosición de la flecha anidada de anterior
.rk-reel-timelineTimelineBarEnvoltorio 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-trackTimelineBarPista (la zona sin reproducir)
.rk-reel-timeline-bufferedTimelineBarCapa de segmentos cargados
.rk-reel-timeline-fillTimelineBarRelleno del progreso reproducido
.rk-reel-timeline-cursorTimelineBarPí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.

TokenPor defectoControla
--rk-reel-overlay-bg#000Color del fondo a pantalla completa
--rk-reel-overlay-z1000z-index del overlay
--rk-reel-button-bgrgba(0, 0, 0, 0.5)Fondo por defecto de los botones circulares
--rk-reel-button-bg-hoverrgba(255, 255, 255, 0.1)Fondo de las flechas de navegación (y estado hover base)
--rk-reel-button-bg-hover-strongrgba(255, 255, 255, 0.2)Fondo de las flechas de navegación al pasar el ratón
--rk-reel-button-fg#fffColor del icono de los botones
--rk-reel-button-size44pxAncho / alto de los botones
--rk-reel-button-radius50%border-radius de los botones
--rk-reel-ui-z10z-index de cerrar / sonido / navegación
--rk-reel-edge-padding16pxSeparación del borde de cerrar / sonido / flechas
--rk-reel-nav-gap8pxEspacio entre las flechas de navegación apiladas
--rk-reel-transition0.2sDuración de la transición al pasar el ratón
--rk-reel-loader-colorrgba(255, 255, 255, 0.12)Color del degradado de la animación de onda
--rk-reel-loader-duration1.8sDuración de la animación de onda
--rk-reel-error-fgrgba(255, 255, 255, 0.4)Color del icono y el texto de error
--rk-reel-slide-overlay-bglinear-gradient(transparent, rgba(0, 0, 0, 0.7))Degradado del fondo del pie
--rk-reel-slide-overlay-padding48px 16px 16pxRelleno interior del pie
--rk-reel-slide-overlay-name-color#fffColor del nombre del autor
--rk-reel-video-bg#000Fondo de las bandas detrás del <video>
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)Fondo de las flechas anidadas
--rk-reel-nested-button-size36pxTamaño de las flechas anidadas
--rk-reel-timeline-trackrgba(255, 255, 255, 0.22)Fondo de la pista (la zona sin reproducir)
--rk-reel-timeline-bufferedrgba(255, 255, 255, 0.4)Color de los segmentos cargados
--rk-reel-timeline-fill#fffColor del relleno del progreso reproducido
--rk-reel-timeline-cursor#fffColor de la píldora de arrastre
--rk-reel-timeline-height3pxAltura de la pista en reposo
--rk-reel-timeline-height-active6pxAltura de la pista al pasar el ratón, con foco o al arrastrar
--rk-reel-timeline-cursor-width10pxAncho de la píldora en reposo
--rk-reel-timeline-cursor-width-active14pxAncho de la píldora al arrastrar
--rk-reel-timeline-cursor-height24pxAltura de la píldora en reposo
--rk-reel-timeline-cursor-height-active32pxAltura de la píldora al arrastrar
--rk-reel-timeline-transition0.15s ease-outAnimació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.

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

TeclaAcción
ArrowUpSlide anterior
ArrowDownSlide siguiente
ArrowLeftMedio anterior (carrusel anidado)
ArrowRightMedio siguiente (carrusel anidado)
EscapeCierra el reproductor