Reel Player
Un componente de reproductor de vídeo a pantalla completa al estilo de Instagram Reels y TikTok, con @reelkit/react-reel-player.
Características
Instalación
npm install @reelkit/react-reel-player @reelkit/react lucide-reactNo olvides importar los estilos:
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.
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
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:
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:
<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:
<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:
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.
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
<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:
<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:
// 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.
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=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.
- 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:
| 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/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:
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.
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.
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.
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
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 ReelPlayerOverlayProps
ReelPlayerOverlayProps<T>
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
apiRef | MutableRefObject<ReelApi> | - | Ref para acceder a la API de Reel |
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 (0.5625) | Relación entre ancho y alto del contenedor del reproductor en escritorio. En móvil el reproductor siempre ocupa todo el viewport. |
content | T[] | obligatorio | Array de elementos de contenido (genérico, ContentItem por defecto) |
initialIndex | number | 0 | Índice del slide inicial |
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 reproductor ya está abierto y el usuario navega. |
isOpen | boolean | obligatorio | Controla 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). |
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. |
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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
controller | UrlStateController | required | Controlador 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
| Prop | Tipo | Descripción |
|---|---|---|
onClose | () => void | Se 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) => void | Se llama después del cambio de slide |
onInnerSlideChange | (outerIndex: number, innerIndex: number) => void | Se 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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
enableNavKeys | boolean | true | Activa la navegación con teclado |
enableWheel | boolean | true | Activa la navegación con la rueda del ratón |
loop | boolean | false | Activa el bucle infinito |
swipeDistanceFactor | number | 0.12 | Umbral del deslizamiento (0-1) |
transitionDuration | number | 300 | Duración de la animación de transición (ms) |
wheelDebounceMs | number | 200 | Duració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.
interface BaseContentItem {
id: string;
media: MediaItem[];
}ContentItem
interface ContentItem extends BaseContentItem {
author: {
name: string;
avatar: string;
};
likes: number;
description: string;
}MediaItem
interface MediaItem {
id: string;
type: 'image' | 'video';
src: string;
poster?: string;
aspectRatio: number;
}MediaType
type MediaType = 'image' | 'video';ControlsRenderProps<T>
interface ControlsRenderProps<T extends BaseContentItem> {
onClose: () => void;
soundState: SoundState;
activeIndex: number;
content: T[];
}NavigationRenderProps
interface NavigationRenderProps {
onPrev: () => void;
onNext: () => void;
activeIndex: number;
count: number;
}SlideRenderProps<T>
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
interface NestedSlideRenderProps {
item: MediaItem;
index: number;
size: [number, number];
isActive: boolean;
isInnerActive: boolean;
slideKey: string;
onVideoRef?: (ref: HTMLVideoElement | null) => void;
defaultContent: ReactNode;
}SlideOverlayProps
interface SlideOverlayProps {
author?: { name: string; avatar: string };
description?: string;
likes?: number;
}ImageSlideProps
interface ImageSlideProps {
src: string;
size: [number, number];
className?: string;
style?: CSSProperties;
imgClassName?: string;
imgStyle?: CSSProperties;
}VideoSlideProps
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
interface CloseButtonProps {
onClick: () => void;
className?: string;
style?: React.CSSProperties;
}SoundButtonProps
interface SoundButtonProps {
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
}TimelineBarProps
interface TimelineBarProps {
className?: string;
style?: React.CSSProperties;
}TimelineRenderProps<T>
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.
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).
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.
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.
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.
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).
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).
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:
| 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 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:
<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 detimelineMinDurationSeconds(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 conrenderTimeline.
<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:
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.
| 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 `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-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.
| 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-error-text-size | 13px | Tamaño de letra del mensaje 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-slide-overlay-description-color | rgba(255, 255, 255, 0.9) | Color del texto de la descripción |
--rk-reel-slide-overlay-likes-color | rgba(255, 255, 255, 0.8) | Color del texto de la fila de me gusta |
--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-bg-hover | rgba(255, 255, 255, 0.2) | Fondo de las flechas anidadas al pasar el ratón |
--rk-reel-nested-button-size | 36px | Tamaño de las flechas anidadas |
--rk-reel-nested-edge-padding | 12px | Separación del borde 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-hitbox | 16px | Zona extra para el puntero encima de la pista |
--rk-reel-timeline-transition | 0.15s ease-out | Animación al crecer y encoger la pista y la píldora |
--rk-reel-timeline-z | 11 | z-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.
/* 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
| Tecla | Acción |
|---|---|
ArrowUp | Slide anterior |
ArrowDown | Slide siguiente |
ArrowLeft | Medio anterior (en el slider anidado) |
ArrowRight | Medio siguiente (en el slider anidado) |
Escape | Cierra el reproductor |