Reel Player

Un componente de reproductor de vídeo a pantalla completa al estilo de Instagram Reels y TikTok, con @reelkit/react-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
Personalizable
Render props para todo
Overlay del slide
Autor, me gusta, descripción
Estado en la URL
Enlaces que se pueden compartir y guardar

Instalación

bash
npm install @reelkit/react-reel-player @reelkit/react lucide-react

No olvides importar los estilos:

typescript
import '@reelkit/react-reel-player/styles.css';
Iconos

Los controles por defecto usan lucide-react para los iconos. Si prefieres otra librería de iconos, usa renderControls y renderNavigation para poner los tuyos.

Inicio rápido

El componente ReelPlayerOverlay renderiza un overlay de reproductor a pantalla completa. Pásale un array de objetos ContentItem y controla su visibilidad con isOpen.

tsx
import { useState } from 'react';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/react-reel-player';
import '@reelkit/react-reel-player/styles.css';

const content: ContentItem[] = [
  {
    id: '1',
    media: [{
      id: 'v1',
      type: 'video',
      src: 'https://example.com/video.mp4',
      poster: 'https://example.com/poster.jpg',
      aspectRatio: 9 / 16,
    }],
    author: { name: 'John Doe', avatar: 'https://example.com/avatar.jpg' },
    likes: 1234,
    description: 'Amazing video!',
  },
];

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Player</button>
      <ReelPlayerOverlay
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        content={content}
      />
    </>
  );
}

Demo en vivo

ReelPlayerPage.tsx

Pulsa una miniatura para abrir el reproductor a pantalla completa. Pulsa Escape o el botón de cerrar para volver.

Personalización

Tipo de contenido genérico

Usa tipos de datos propios extendiendo BaseContentItem:

tsx
import { ReelPlayerOverlay, type BaseContentItem } from '@reelkit/react-reel-player';

interface MyItem extends BaseContentItem {
  title: string;
  username: string;
}

const items: MyItem[] = [
  {
    id: '1',
    media: [{ id: 'v1', type: 'video', src: '/video.mp4', aspectRatio: 9/16 }],
    title: 'My Video',
    username: '@user',
  },
];

<ReelPlayerOverlay<MyItem>
  isOpen={isOpen}
  onClose={handleClose}
  content={items}
  renderSlideOverlay={(item) => (
    <div style={{ position: 'absolute', bottom: 16, left: 16, color: '#fff' }}>
      <strong>{item.username}</strong>
      <p>{item.title}</p>
    </div>
  )}
/>

Overlay del slide propio

Sustituye el overlay del slide incluido por contenido propio en cada slide:

tsx
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderSlideOverlay={(item, index, isActive) => (
    <div style={{
      position: 'absolute',
      bottom: 0,
      left: 0,
      right: 0,
      padding: 16,
      background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
      color: '#fff',
    }}>
      <h3>{item.author.name}</h3>
      <p>{item.description}</p>
      <span>Slide {index + 1} {isActive ? '(active)' : ''}</span>
    </div>
  )}
/>

Slides sin medios

Usa renderSlide para insertar contenido propio (por ejemplo, tarjetas de llamada a la acción). Devuelve null para usar el slide por defecto:

tsx
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderSlide={({ index, size }) => {
    // CTA card on last slide
    if (index === content.length - 1) {
      return (
        <div style={{
          width: size[0],
          height: size[1],
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          background: 'linear-gradient(135deg, #667eea, #764ba2)',
          color: '#fff',
        }}>
          <div style={{ textAlign: 'center' }}>
            <h2>Follow for more!</h2>
            <button>Subscribe</button>
          </div>
        </div>
      );
    }
    // Fall back to default MediaSlide + overlay
    return null;
  }}
/>

Controles propios

Combina los subcomponentes reutilizables con tus propios añadidos:

tsx
import {
  ReelPlayerOverlay,
  CloseButton,
  SoundButton,
} from '@reelkit/react-reel-player';

<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderControls={({ onClose, content, activeIndex }) => (
    <>
      <CloseButton onClick={onClose} />
      <SoundButton />
      <button
        onClick={() => share(content[activeIndex])}
        style={{
          position: 'absolute',
          bottom: 60,
          right: 16,
          zIndex: 10,
        }}
      >
        Share
      </button>
    </>
  )}
/>

Línea de tiempo propia

Sustituye la barra de reproducción incluida por tu propia interfaz de arrastre con renderTimeline. El callback solo se llama cuando las reglas del overlay renderizarían la barra por defecto (la misma lógica del 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 al overlay del slide en dispositivos táctiles.

tsx
import { useRef, useEffect } from 'react';
import { ReelPlayerOverlay } from '@reelkit/react-reel-player';
import { Observe } from '@reelkit/react';

function CustomTimelineBar({ timelineState }) {
  const trackRef = useRef(null);
  useEffect(() => {
    if (!trackRef.current) return;
    // Pointer + keyboard scrub wiring, same as the built-in bar.
    return timelineState.bindInteractions(trackRef.current);
  }, [timelineState]);

  return (
    <div className="rk-reel-timeline" style={{ padding: '0 16px' }}>
      <Observe signals={[timelineState.progress, timelineState.currentTime]}>
        {() => (
          <div
            ref={trackRef}
            role="slider"
            aria-valuenow={timelineState.currentTime.value}
            style={{ height: 6, background: 'rgba(255,255,255,0.2)' }}
          >
            <div style={{
              width: `${timelineState.progress.value * 100}%`,
              height: '100%',
              background: 'linear-gradient(90deg, #6366f1, #ec4899)',
            }} />
          </div>
        )}
      </Observe>
    </div>
  );
}

<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  timeline="always"
  renderTimeline={({ timelineState }) => (
    <CustomTimelineBar timelineState={timelineState} />
  )}
/>

Navegación propia

tsx
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderNavigation={({ onPrev, onNext, activeIndex, count }) => (
    <div style={{ position: 'absolute', right: 16, top: '50%', transform: 'translateY(-50%)' }}>
      <button onClick={onPrev} disabled={activeIndex === 0}>Up</button>
      <span>{activeIndex + 1}/{count}</span>
      <button onClick={onNext} disabled={activeIndex === count - 1}>Down</button>
    </div>
  )}
/>

Navegación anidada propia

Sustituye las flechas de izquierda y derecha de los slides con varios medios (carrusel horizontal) por una navegación propia:

tsx
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderNestedNavigation={({ onPrev, onNext, activeIndex, count }) => (
    <div style={{
      position: 'absolute',
      bottom: 48,
      left: 0,
      right: 0,
      display: 'flex',
      justifyContent: 'center',
      gap: 8,
      zIndex: 10,
    }}>
      <button onClick={onPrev} disabled={activeIndex === 0}>Prev</button>
      <span>{activeIndex + 1} / {count}</span>
      <button onClick={onNext} disabled={activeIndex === count - 1}>Next</button>
    </div>
  )}
/>

Slides anidados propios

Personaliza cada slide de los carruseles con varios medios con renderNestedSlide. Usa props.defaultContent para envolver el ImageSlide/VideoSlide por defecto o sustitúyelo por completo:

tsx
// Wrap default slides with rounded corners
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderNestedSlide={({ defaultContent }) => (
    <div style={{ borderRadius: 16, overflow: 'hidden' }}>
      {defaultContent}
    </div>
  )}
/>

// Fully custom nested slide for images
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderNestedSlide={({ item, size, isActive, slideKey, onVideoRef, defaultContent }) => {
    if (item.type === 'video') return defaultContent; // keep default video
    return (
      <ImageSlide
        src={item.src}
        size={size}
        imgStyle={{ objectFit: 'contain' }}
        style={{ backgroundColor: '#111' }}
      />
    );
  }}
/>

Estado en la URL

Ver la demo en vivo →

ReelPlayerUrlOverlay es un componente aparte cuyo estado abierto vive en la barra de direcciones. Crea un controlador con useOverlayUrlState de @reelkit/react y pásalo como controller: el reproductor se abre solo cuando el parámetro nombra un slide y se cierra cuando desaparece. Los enlaces se pueden compartir y el botón de volver cierra el reproductor en lugar de salir de la página.

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/react. Consulta la guía del estado en la URL y la API del core.

tsx
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/react';
import { ReelPlayerUrlOverlay } from '@reelkit/react-reel-player';
import { Link } from 'react-router-dom';

const reel = useOverlayUrlState({
  param: 'reel',
  ...urlIndexKey(() => content.length),
});

// Opening is a link — the overlay reads the URL and opens itself.
{content.map((item, i) => (
  <Link key={item.id} to={`?reel=${i}`}>
    <img src={getThumbnail(item)} />
  </Link>
))}

<ReelPlayerUrlOverlay controller={reel} content={content} />

Las opciones completas de useOverlayUrlState (param, adapter, codec, locator) están en la referencia de la API de React. La explicación paso a paso está en la guía de React.

  • 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.
  • Por defecto el parámetro solo apunta a la publicación vertical (?reel=3). Usa una clave de dos ejes para llevar también el índice del medio interior de un carrusel con varios medios; mira más abajo.

Una clave o dos: elige la profundidad de la URL

El mismo ReelPlayerUrlOverlay 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.

tsx
import { useOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/react';

const reel = useOverlayUrlState({
  param: 'reel',
  ...urlIndexTwoAxisKey({
    outerCount: () => content.length,
    innerCounts: () => content.map((post) => post.media.length),
  }),
});

// A link now names both axes: post 3, inner media 2.
<Link to="?reel=3.2">…</Link>
<ReelPlayerUrlOverlay controller={reel} content={content} />

Aplicación con router: pasa un adaptador. Escribir con history.pushState a espaldas de un router deja su ubicación desfasada, y su siguiente navegación pierde el parámetro:

tsx
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';

const adapter = useReactRouterUrlAdapter();
const reel = useOverlayUrlState({
  param: 'reel',
  adapter,
  ...urlIndexKey(() => content.length),
});

<ReelPlayerUrlOverlay controller={reel} content={content} />

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.

tsx
const reel = useOverlayUrlState({
  param: 'reel',
  ...urlStableIdKey({ items: () => content }),
});

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.

tsx
const reel = useOverlayUrlState({
  param: 'reel',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => content.findIndex((x) => x.id === id),
    identify: (index) => content[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.

tsx
const reel = useOverlayUrlState({
  param: 'reel',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => content.findIndex((x) => x.id === id),
    identify: (index) => content[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
      setContent(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 ReelPlayerOverlayProps

ReelPlayerOverlayProps<T>

PropTipoPor defectoDescripción
apiRefMutableRefObject<ReelApi>-Ref para acceder a la API de Reel
ariaLabelstring'Video player'Etiqueta accesible de la región del diálogo; los lectores de pantalla la anuncian al abrirse el overlay
aspectRationumber9/16 (0.5625)Relación entre ancho y alto del contenedor del reproductor en escritorio. En móvil el reproductor siempre ocupa todo el viewport.
contentT[]obligatorioArray de elementos de contenido (genérico, ContentItem por defecto)
initialIndexnumber0Índice del slide inicial
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 reproductor ya está abierto y el usuario navega.
isOpenbooleanobligatorioControla la visibilidad del overlay. Para que el estado abierto lo controle la URL, usa el componente aparte ReelPlayerUrlOverlay; mira el estado en la URL más arriba.
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 renderTimeline 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.
renderControls(props: ControlsRenderProps) => ReactNode-Controles propios; sustituyen los botones por defecto de cerrar y de sonido
renderError(props: { item: T; activeIndex: number }) => ReactNode-Indicador de error propio; sustituye el icono de error por defecto
renderLoading(props: { item: T; activeIndex: number }) => ReactNode-Indicador de carga propio; sustituye la animación de onda por defecto
renderNavigation(props: NavigationRenderProps) => ReactNode-Navegación propia; sustituye las flechas verticales por defecto
renderNestedNavigation(props: NavigationRenderProps) => ReactNode-Navegación propia del slider horizontal anidado (publicaciones con varios medios); sustituye las flechas de izquierda y derecha por defecto
renderNestedSlide(props: NestedSlideRenderProps) => ReactNode-Renderizador propio de los elementos del slider horizontal anidado. Usa props.defaultContent para envolver o incluir el ImageSlide/VideoSlide por defecto. A diferencia de renderSlide, null no hace que se use el slide por defecto.
renderSlide(props: SlideRenderProps) => ReactNode | null-Renderizado propio del slide. Devuelve null para usar el slide por defecto. Usa props.defaultContent para envolver o incluir el slide por defecto.
renderSlideOverlay(item, index, isActive) => ReactNode-Overlay propio por slide; sustituye el SlideOverlay por defecto. Devuelve null para ocultarlo.
renderTimeline(props: TimelineRenderProps) => ReactNode-Barra de reproducción propia. Solo se llama cuando las reglas renderizarían la barra por defecto (la misma lógica de auto/always/never y timelineMinDurationSeconds). Usa props.defaultContent para envolver el <TimelineBar /> incluido; devuelve null para ocultarla.

Props de ReelPlayerUrlOverlay

ReelPlayerUrlOverlayProps<T>

Acepta todas las props visuales y de comportamiento de arriba salvo isOpen, que se sustituye por controller. initialIndex 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 posición 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.

Callbacks

PropTipoDescripción
onClose() => voidSe llama al cerrar el reproductor. Obligatorio en ReelPlayerOverlay (el estado abierto es tuyo, así que tienes que gestionar el cierre); opcional en ReelPlayerUrlOverlay, donde la URL controla el cierre: pásalo solo para reaccionar después de cerrar.
onSlideChange(index: number) => voidSe llama después del cambio de slide
onInnerSlideChange(outerIndex: number, innerIndex: number) => voidSe llama cuando cambia el índice del medio interior de la publicación activa: al navegar dentro de una publicación con varios medios y al activar una publicación, con su índice interior actual (0 en una publicación con un solo medio).

Props de Reel (reenviadas)

Estas props se reenvían al componente Reel que hay debajo.

PropTipoPor defectoDescripción
enableNavKeysbooleantrueActiva la navegación con teclado
enableWheelbooleantrueActiva la navegación con la rueda del ratón
loopbooleanfalseActiva el bucle infinito
swipeDistanceFactornumber0.12Umbral del deslizamiento (0-1)
transitionDurationnumber300Duración de la animación de transición (ms)
wheelDebounceMsnumber200Duración del debounce de la rueda (ms)

Tipos

BaseContentItem

El tipo de restricción genérica. Extiéndelo para usar tipos de datos propios con ReelPlayerOverlay.

typescript
interface BaseContentItem {
  id: string;
  media: MediaItem[];
}

ContentItem

typescript
interface ContentItem extends BaseContentItem {
  author: {
    name: string;
    avatar: string;
  };
  likes: number;
  description: string;
}

MediaItem

typescript
interface MediaItem {
  id: string;
  type: 'image' | 'video';
  src: string;
  poster?: string;
  aspectRatio: number;
}

MediaType

typescript
type MediaType = 'image' | 'video';

ControlsRenderProps<T>

typescript
interface ControlsRenderProps<T extends BaseContentItem> {
  onClose: () => void;
  soundState: SoundState;
  activeIndex: number;
  content: T[];
}
typescript
interface NavigationRenderProps {
  onPrev: () => void;
  onNext: () => void;
  activeIndex: number;
  count: number;
}

SlideRenderProps<T>

typescript
interface SlideRenderProps<T extends BaseContentItem> {
  item: T;
  index: number;
  size: [number, number];
  isActive: boolean;
  slideKey: string;
  onVideoRef?: (ref: HTMLVideoElement | null) => void;
  innerSliderRef: MutableRefObject<ReelApi | null>;
  onActiveMediaTypeChange?: (type: 'image' | 'video') => void;
  renderNestedNavigation?: (props: NavigationRenderProps) => ReactNode;
  enableWheel?: boolean;
  defaultContent: ReactNode;
}

NestedSlideRenderProps

typescript
interface NestedSlideRenderProps {
  item: MediaItem;
  index: number;
  size: [number, number];
  isActive: boolean;
  isInnerActive: boolean;
  slideKey: string;
  onVideoRef?: (ref: HTMLVideoElement | null) => void;
  defaultContent: ReactNode;
}

SlideOverlayProps

typescript
interface SlideOverlayProps {
  author?: { name: string; avatar: string };
  description?: string;
  likes?: number;
}

ImageSlideProps

typescript
interface ImageSlideProps {
  src: string;
  size: [number, number];
  className?: string;
  style?: CSSProperties;
  imgClassName?: string;
  imgStyle?: CSSProperties;
}

VideoSlideProps

typescript
interface VideoSlideProps {
  src: string;
  poster?: string;
  aspectRatio: number;
  size: [number, number];
  isActive: boolean;
  isInnerActive?: boolean;   // default: true
  slideKey: string;
  onVideoRef?: (ref: HTMLVideoElement | null) => void;
  className?: string;
  style?: CSSProperties;
}

CloseButtonProps

typescript
interface CloseButtonProps {
  onClick: () => void;
  className?: string;
  style?: React.CSSProperties;
}

SoundButtonProps

typescript
interface SoundButtonProps {
  disabled?: boolean;
  className?: string;
  style?: React.CSSProperties;
}

TimelineBarProps

typescript
interface TimelineBarProps {
  className?: string;
  style?: React.CSSProperties;
}

TimelineRenderProps<T>

typescript
interface TimelineRenderProps<T extends BaseContentItem> {
  item: T;
  activeIndex: number;
  timelineState: TimelineController;
  defaultContent: ReactNode;
}

Subcomponentes

Piezas reutilizables exportadas para combinarlas en render props propias:

CloseButton

Botón de cerrar independiente con el estilo por defecto del reproductor. Úsalo dentro de renderControls.

tsx
import { CloseButton } from '@reelkit/react-reel-player';

<CloseButton onClick={onClose} />
<CloseButton onClick={onClose} className="my-close-btn" style={{ top: 24, right: 24 }} />

SoundButton

Botón de sonido independiente. Debe estar dentro de un SoundProvider (ReelPlayerOverlay lo proporciona automáticamente).

tsx
import { SoundButton } from '@reelkit/react-reel-player';

<SoundButton />
<SoundButton disabled className="my-sound-btn" />

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

tsx
import { TimelineBar } from '@reelkit/react-reel-player';

// Inside renderTimeline — wrap or augment the default bar:
<ReelPlayerOverlay
  renderTimeline={({ defaultContent }) => (
    <>
      <MyTimecode />
      {defaultContent}
    </>
  )}
/>

// Or render standalone inside a custom TimelineProvider tree:
<TimelineBar className="my-timeline" />

SlideOverlay

El overlay con degradado por defecto que muestra el autor, la descripción y los me gusta. Se renderiza automáticamente cuando el contenido tiene los campos necesarios. Usa renderSlideOverlay para sustituirlo u ocultarlo.

tsx
import { SlideOverlay } from '@reelkit/react-reel-player';

<SlideOverlay
  author={{ name: 'John', avatar: '/avatar.jpg' }}
  description="Amazing content"
  likes={12500}
/>

ImageSlide

Slide de imagen con carga diferida y object-fit: cover por defecto. Úsalo dentro de renderSlide para crear slides de imagen propios con tus estilos.

tsx
import { ImageSlide } from '@reelkit/react-reel-player';

// Default usage
<ImageSlide src="/photo.jpg" size={[400, 700]} />

// Custom styles
<ImageSlide
  src="/photo.jpg"
  size={[400, 700]}
  className="my-image-slide"
  style={{ backgroundColor: '#1a1a1a', borderRadius: 12 }}
  imgStyle={{ objectFit: 'contain' }}
/>

VideoSlide

Slide de vídeo con un elemento <video> compartido para que el sonido no se corte en iOS, fotogramas de póster, memoria de la posición e indicador de carga. Debe estar dentro de un SoundProvider (ReelPlayerOverlay lo proporciona automáticamente).

tsx
import { VideoSlide } from '@reelkit/react-reel-player';

<VideoSlide
  src="/video.mp4"
  poster="/thumb.jpg"
  aspectRatio={9 / 16}
  size={[400, 700]}
  isActive={true}
  slideKey="slide-1"
  style={{ borderRadius: 12 }}
/>
Crear slides propios

Usa renderSlide 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).

tsx
import {
  ReelPlayerOverlay,
  ImageSlide,
  VideoSlide,
} from '@reelkit/react-reel-player';

<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={handleClose}
  content={content}
  renderSlide={({ item, size, isActive, slideKey, onVideoRef }) => {
    const media = item.media[0];
    if (media.type === 'image') {
      return (
        <ImageSlide
          src={media.src}
          size={size}
          imgStyle={{ objectFit: 'contain' }}
          style={{ backgroundColor: '#111' }}
        />
      );
    }
    if (media.type === 'video') {
      return (
        <VideoSlide
          src={media.src}
          poster={media.poster}
          aspectRatio={media.aspectRatio}
          size={size}
          isActive={isActive}
          slideKey={slideKey}
          onVideoRef={onVideoRef}
          style={{ borderRadius: 16 }}
        />
      );
    }
    return null; // fallback to default
  }}
/>

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. Las URLs con error se guardan en caché, así que al volver se muestra el error al instante sin reintentar.

Callbacks del ciclo de vida

Si usas renderSlide, llama a estos callbacks 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.
tsx
// Inside renderSlide — wire callbacks to your custom media
renderSlide={({ item, size, isActive, onReady, onWaiting, onError }) => (
  <div style={{ width: size[0], height: size[1] }}>
    {item.media[0].type === 'image' ? (
      <img
        src={item.media[0].src}
        onLoad={onReady}
        onError={onError}
        style={{ width: '100%', height: '100%', objectFit: 'cover' }}
      />
    ) : (
      <video
        src={item.media[0].src}
        autoPlay={isActive}
        onCanPlay={onReady}
        onWaiting={onWaiting}
        onError={onError}
        style={{ width: '100%', height: '100%', objectFit: 'cover' }}
      />
    )}
  </div>
)}

Interfaz de carga y de error propia

Sustituye la animación de onda y el icono de error por defecto por componentes propios:

tsx
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  content={content}
  renderLoading={({ item, activeIndex }) => (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 10,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: '#fff', fontSize: 14,
    }}>
      Loading slide {activeIndex + 1}...
    </div>
  )}
  renderError={({ item, activeIndex }) => (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 10,
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center',
      gap: 12, color: 'rgba(255,255,255,0.5)',
    }}>
      <span style={{ fontSize: 48 }}>!</span>
      <span>Failed to load media</span>
    </div>
  )}
/>

Línea de tiempo

El overlay renderiza una barra de reproducción incluida sobre el vídeo activo. La prop timeline decide cuándo:

  • 'auto' (por defecto): se renderiza cuando el medio activo es un vídeo que dura más de timelineMinDurationSeconds (30 por defecto). Funciona con slides de un solo vídeo y con carruseles de varios medios; la barra sigue al elemento anidado activo y se oculta en las imágenes.
  • 'always': se renderiza siempre que el slide activo tenga un vídeo.
  • 'never': nunca se renderiza. Crea una barra propia con renderTimeline.
tsx
<ReelPlayerOverlay
  isOpen={isOpen}
  onClose={close}
  content={items}
  timeline="auto"
  timelineMinDurationSeconds={30}
/>

Cambia su tema con las propiedades personalizadas de CSS --rk-reel-timeline-* (altura, colores, tamaño del cursor). Para una barra de arrastre, un contador de tiempo o un indicador de progreso totalmente propios, usa renderTimeline; el callback recibe un timelineState respaldado por el TimelineController que hay debajo.

Contexto de sonido

En implementaciones propias, puedes acceder al estado del sonido:

tsx
import { SoundProvider, useSoundState } from '@reelkit/react';

// ReelPlayerOverlay wraps itself in a SoundProvider.
// Access sound state inside custom controls:
function CustomControls() {
  const soundState = useSoundState();

  return (
    <button onClick={soundState.toggle}>
      {soundState.muted.value ? 'Unmute' : 'Mute'}
    </button>
  );
}

Clases CSS

Todas las clases CSS son normales (no CSS modules), así que puedes apuntar a ellas con selectores más específicos en una hoja de estilos cargada después de @reelkit/react-reel-player/styles.css. Para cambiar colores, tamaños y z-index, es mejor usar las propiedades personalizadas de CSS de la sección Temas de más abajo: están pensadas justo para eso.

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 `renderTimeline` 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.

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-error-text-size13pxTamaño de letra del mensaje 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-slide-overlay-description-colorrgba(255, 255, 255, 0.9)Color del texto de la descripción
--rk-reel-slide-overlay-likes-colorrgba(255, 255, 255, 0.8)Color del texto de la fila de me gusta
--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-bg-hoverrgba(255, 255, 255, 0.2)Fondo de las flechas anidadas al pasar el ratón
--rk-reel-nested-button-size36pxTamaño de las flechas anidadas
--rk-reel-nested-edge-padding12pxSeparación del borde 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-hitbox16pxZona extra para el puntero encima de la pista
--rk-reel-timeline-transition0.15s ease-outAnimación al crecer y encoger la pista y la píldora
--rk-reel-timeline-z11z-index de la línea de tiempo (por encima de la capa de interfaz por defecto)

Pega el fragmento de abajo en una hoja de estilos cargada después de @reelkit/react-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 ariaLabel para cambiar lo que anuncia el lector de pantalla; por defecto es "Video player". Cada slide lleva role="group", aria-roledescription="slide" y un aria-label="Slide N of M", así que al deslizar se anuncia la posición en la secuencia.

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 (en el slider anidado)
ArrowRightMedio siguiente (en el slider anidado)
EscapeCierra el reproductor