Reel Player
Um player de vídeo em tela cheia no estilo Instagram Reels/TikTok, com @reelkit/react-reel-player.
Recursos
Instalação
npm install @reelkit/react-reel-player @reelkit/react lucide-reactNão esqueça de importar os estilos:
import '@reelkit/react-reel-player/styles.css';Ícones
Os controles padrão usam lucide-react para os ícones. Se você prefere outra biblioteca de ícones, use renderControls e renderNavigation para fornecer os seus.
Início rápido
O componente ReelPlayerOverlay renderiza um player em tela cheia. Passe um array de objetos ContentItem e controle a visibilidade com 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}
/>
</>
);
}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: '/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-04.mp4',
poster: '/cdn/samples/videos/video-poster-04.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'James Wilson', avatar: '/cdn/samples/avatars/avatar-03.jpg' },
likes: 3456,
description: 'City life adventures',
},
{
id: '4',
media: [
{ id: 'img2', type: 'image', src: '/cdn/samples/images/image-02.jpg', aspectRatio: 2 / 3 },
{ id: 'img3', type: 'image', src: '/cdn/samples/images/image-03.jpg', aspectRatio: 3 / 4 },
],
author: { name: 'Emma Davis', avatar: '/cdn/samples/avatars/avatar-04.jpg' },
likes: 8901,
description: 'Travel moments',
},
{
id: '5',
media: [{
id: 'img4',
type: 'image',
src: '/cdn/samples/images/image-04.jpg',
aspectRatio: 2 / 3,
}],
author: { name: 'Michael Brown', avatar: '/cdn/samples/avatars/avatar-05.jpg' },
likes: 2345,
description: 'Golden hour magic',
},
{
id: '6',
media: [{
id: 'v3',
type: 'video',
src: '/cdn/samples/videos/video-05.mp4',
poster: '/cdn/samples/videos/video-poster-05.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Alex Johnson', avatar: '/cdn/samples/avatars/avatar-01.jpg' },
likes: 7890,
description: 'Living the moment',
},
];
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [initialIndex, setInitialIndex] = useState(0);
return (
<div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
{content.map((item, i) => (
<button
key={item.id}
onClick={() => { setInitialIndex(i); setIsOpen(true); }}
style={{
position: 'relative', aspectRatio: '9 / 16', borderRadius: 8,
overflow: 'hidden', border: 'none', padding: 0, cursor: 'pointer',
background: '#1e293b',
}}
>
<img
src={item.media[0].poster || item.media[0].src}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</button>
))}
</div>
<ReelPlayerOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={content}
initialIndex={initialIndex}
/>
</div>
);
}Para o fluxo completo, uma grade de miniaturas que abre o player e leva o usuário de volta aonde parou, veja a receita Da grade ao player.
Personalização
Tipo de conteúdo genérico
Use tipos de dados próprios estendendo 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>
)}
/>Camada própria sobre o slide
Troque a camada padrão por um conteúdo seu em 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 que não são mídia
Use renderSlide para inserir conteúdo próprio (cartões de chamada, por exemplo). Devolva null para cair no padrão:
<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 próprios
Componha os subcomponentes reutilizáveis com os seus acréscimos:
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>
</>
)}
/>Linha do tempo própria
Troque a barra de reprodução padrão pela sua, com renderTimeline. O callback só é chamado quando as regras do overlay renderizariam a barra padrão (a mesma lógica de modo timeline e de timelineMinDurationSeconds), então você não precisa reimplementá-las. Reaproveite a classe .rk-reel-timeline na sua raiz para herdar o posicionamento rente à base, o respiro da área segura e o espaço reservado para a camada do slide em dispositivos de toque.
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} />
)}
/>Navegação própria
<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>
)}
/>Navegação aninhada própria
Troque as setas de esquerda e direita dentro dos slides com várias mídias (carrossel horizontal) por uma navegação sua:
<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 aninhados próprios
Personalize cada slide dentro dos carrosséis de várias mídias com renderNestedSlide. Use props.defaultContent para envolver o ImageSlide/VideoSlide padrão, ou substitua-o 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 na URL
Ver demonstração ao vivo →ReelPlayerUrlOverlay é um componente à parte cujo estado aberto vive na barra de endereços. Monte um controlador com useOverlayUrlState, de @reelkit/react, e entregue-o como controller: o player se abre quando o parâmetro nomeia um slide e se fecha quando ele some. Os links dão para compartilhar, e o botão voltar fecha o player em vez de sair da página.
Chaves prontas
Você pode endereçar os slides com uma chave pronta — espalhe urlIndexKey (por posição) ou urlStableIdKey (por um id estável) no controlador — as duas reexportadas por @reelkit/react. Veja o guia de estado na URL e a API do 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} />As opções completas de useOverlayUrlState — param, adapter, codec, locator — estão na referência da API para React. O passo a passo está no guia para React.
- Abrir empilha uma entrada no histórico. Percorrer o feed substitui essa entrada, então N deslizes não acrescentam nenhuma e um único passo para trás sempre sai do player. Voltar fecha; não troca de slide.
- Voltar só fecha quando o player foi aberto de dentro do app — foi o link que empilhou a entrada. Um link compartilhado aberto direto em uma aba nova não tem histórico atrás de si, então o voltar do navegador sai do site; o botão ✕ ou o Escape removem o parâmetro ali mesmo e você continua na página.
- Um link direto
?reel=3abre o player naquele slide já no carregamento. - Um parâmetro que não nomeia slide nenhum — um favorito velho, um valor editado à mão — é retirado da URL, em vez de deixar a barra de endereços afirmando um slide que não abre.
- Por padrão o parâmetro endereça apenas o post vertical (
?reel=3). Adote uma chave de dois eixos para carregar também o índice da mídia interna de um carrossel — veja abaixo.
Uma chave ou duas — escolha a profundidade da URL
O mesmo ReelPlayerUrlOverlay conduz os dois formatos; ele distingue em tempo de execução pela posição do controlador, então não existe prop de modo. Escolha a chave ao montar o controlador:
| Chave | Fio | O que carrega |
|---|---|---|
urlIndexKey(…) | ?reel=3 | Apenas o post vertical. |
urlIndexTwoAxisKey(…) | ?reel=3.2 | O post e o índice da mídia interna do carrossel. |
Os dois fios são deliberadamente distintos — uma chave de dois eixos é estritamente pontuada (3.0, nunca um 3 solto), então um link de um eixo não é decodificado pelo outro. Trocar a chave de um app, portanto, invalida qualquer link já compartilhado. Escolha um formato e fique com ele.
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} />App com rotas — passe um adaptador. Escrever history.pushState por trás de um roteador deixa a localização dele desatualizada, e a navegação seguinte derruba o 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} />Links estáveis. O índice é posicional, então um ?reel=3 favoritado abre outro post assim que o feed muda de ordem — e num feed isso é o normal, não a exceção. urlStableIdKey usa o id estável de cada post, varrendo o feed atual — uma chamada resolve o caso comum.
const reel = useOverlayUrlState({
param: 'reel',
...urlStableIdKey({ items: () => content }),
});Passe hashCodec: base64UrlCodec para escrever o id em base64url na URL — é um disfarce reversível, não um hash criptográfico.
Quer endereçar por outro campo (um slug), ou paginar um feed infinito com locateAsync? Monte o codec e o locator você mesmo. São dois papéis distintos: o codec escreve a identidade na URL, o locator descobre onde ela está.
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 é síncrono, então só responde pelos posts já carregados — um link compartilhado para o post 400 de um feed que carregou 20 volta vazio. locateAsync é o plano B, chamado somente quando locate falha.
Atalho
Vai endereçar pelo id do item? Dispense o codec e o locator escritos à mão — passe locateAsync direto para urlStableIdKey({ items, locateAsync }) (ele busca quando falha e então devolve o índice). A versão mais completa abaixo serve para endereçar por outro campo, ou para ter controle 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
},
},
});- Enquanto
locateAsyncestá pendente, o player fica fechado e o parâmetro é deixado em paz, então o link direto sobrevive à busca. Umnullou uma rejeição derrubam o parâmetro. - Uma resposta que chega depois de a URL ter mudado, depois de um fechamento ou depois da desmontagem é descartada — uma busca lenta não pode abrir um slide que ninguém pediu.
- Nada é renderizado enquanto a busca corre; esse estado de carregamento já pertence à página, então desenhe o seu próprio esqueleto.
- Não existe tempo limite — o player não tem como saber o tamanho do feed. Termine com
nullquando a paginação se esgotar, ou o overlay fica fechado para sempre.
Referência da API
Props do ReelPlayerOverlayProps
ReelPlayerOverlayProps<T>
ReelPlayerOverlayProps é ReelPlayerOverlayBaseProps (as props visuais e de comportamento que os dois overlays compartilham) unido a ReelPlayerControlledProps (isOpen e onClose). Use o tipo base para tipar um wrapper que renderize qualquer um dos overlays.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
apiRef | MutableRefObject<ReelApi> | - | Ref para alcançar a API do Reel |
ariaLabel | string | 'Video player' | Rótulo acessível da região do diálogo; anunciado pelos leitores de tela quando o overlay abre |
aspectRatio | number | 9/16 (0.5625) | Proporção entre largura e altura do contêiner do player no desktop. Com 768px de largura ou menos o player sempre ocupa a tela inteira. Um valor zero, negativo ou não finito volta para 9/16. |
content | T[] | obrigatório | Array de itens de conteúdo (genérico, ContentItem por padrão) |
initialIndex | number | 0 | Índice do slide inicial |
initialInnerIndex | number | 0 | Índice da mídia interna em que abrir, apenas para o post visível no início — permite que uma URL de dois eixos aponte direto para uma imagem específica de um post com várias mídias. Ignorado assim que o player abre e o leitor navega. |
isOpen | boolean | obrigatório | Controla a visibilidade do overlay. Para um estado aberto guiado pela URL, use o componente à parte ReelPlayerUrlOverlay — veja Estado na URL, abaixo. |
timeline | 'auto' | 'always' | 'never' | 'auto' | Estratégia que libera a barra de reprodução embutida. 'auto' só renderiza para vídeos mais longos que timelineMinDurationSeconds; 'always' renderiza sempre que o slide ativo tiver vídeo; 'never' desliga a barra embutida (use renderTimeline para substituí-la inteiramente). |
timelineMinDurationSeconds | number | 30 | Duração mínima do vídeo, em segundos, para que timeline='auto' renderize a barra embutida. Clipes curtos em repetição abaixo desse limite ficam sem barra. |
renderControls | (props: ControlsRenderProps) => ReactNode | - | Controles próprios, substituem os botões padrão de fechar e som |
renderError | (props: { item: T; activeIndex: number }) => ReactNode | - | Indicador de erro próprio, substitui o ícone padrão de erro |
renderLoading | (props: { item: T; activeIndex: number }) => ReactNode | - | Indicador de carregamento próprio, substitui a animação padrão de onda |
renderNavigation | (props: NavigationRenderProps) => ReactNode | - | Navegação própria, substitui as setas verticais padrão |
renderNestedNavigation | (props: NavigationRenderProps) => ReactNode | - | Navegação própria do slider horizontal aninhado (posts com várias mídias), substitui as setas padrão de esquerda e direita |
renderNestedSlide | (props: NestedSlideRenderProps) => ReactNode | - | Renderizador próprio dos itens do slider horizontal aninhado. Use props.defaultContent para envolver ou embutir o ImageSlide/VideoSlide padrão. Diferente de renderSlide, aqui null não significa cair no padrão. |
renderSlide | (props: SlideRenderProps) => ReactNode | null | - | Renderização própria do slide. Devolva null para cair no padrão. Use props.defaultContent para envolver ou embutir o slide padrão. |
renderSlideOverlay | (item, index, isActive) => ReactNode | - | Camada própria em cada slide, substitui o SlideOverlay padrão. Devolva null para esconder. |
renderTimeline | (props: TimelineRenderProps) => ReactNode | - | Barra de reprodução própria. Chamada apenas quando as regras liberariam a barra padrão (a mesma lógica de auto/always/never e de timelineMinDurationSeconds). Use props.defaultContent para envolver a <TimelineBar /> embutida; devolva null para esconder. |
Props do ReelPlayerUrlOverlay
ReelPlayerUrlOverlayProps<T>
Aceita todas as props visuais e de comportamento acima, menos isOpen, substituída por controller: o tipo é ReelPlayerOverlayBaseProps unido a ReelPlayerUrlControlledProps. initialIndex é ignorada — quem escolhe o slide é a posição do controlador, então um valor passado ao lado dela é sobrescrito a cada abertura.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
controller | UrlStateController | obrigatório | Controlador vindo de useOverlayUrlState. A posição dele decide se o overlay está aberto e qual slide aparece; o overlay escreve de volta por ele na troca de slide e no fechamento. |
Callbacks
| Prop | Tipo | Descrição |
|---|---|---|
onClose | () => void | Chamado quando o player fecha. Obrigatório no ReelPlayerOverlay (o estado aberto é seu, então o fechamento também); opcional no ReelPlayerUrlOverlay, onde quem fecha é a URL — passe apenas se quiser reagir depois do fechamento. |
onSlideChange | (index: number) => void | Chamado depois da troca de slide |
onInnerSlideChange | (outerIndex: number, innerIndex: number) => void | Chamado quando o índice da mídia interna do post ativo muda — na navegação interna de um post com várias mídias e na ativação externa, informando o índice interno atual do post ativado (0 para um post de mídia única). |
Props do Reel (repassadas)
Estas props são repassadas ao componente Reel por baixo. O tipo delas é ReelProxyProps, o subconjunto das props do Reel que o overlay repassa; use-o para tipar um wrapper que as encaminha.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
enableNavKeys | boolean | true | Liga a navegação por teclado |
enableWheel | boolean | true | Liga a navegação pela roda do mouse |
loop | boolean | false | Liga o ciclo infinito |
swipeDistanceFactor | number | 0.12 | Limiar do deslize (0-1) |
transitionDuration | number | 300 | Duração da animação de transição (ms) |
wheelDebounceMs | number | 200 | Debounce da roda do mouse (ms) |
Tipos
BaseContentItem
O tipo que serve de restrição genérica. Estenda-o para usar tipos de dados próprios com o 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
Blocos reutilizáveis exportados para você compor dentro das suas render props:
CloseButton
Botão de fechar isolado, já com o estilo do reel player. Use 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ão de som isolado. Precisa estar dentro de um SoundProvider (que o ReelPlayerOverlay fornece sozinho).
import { SoundButton } from '@reelkit/react-reel-player';
<SoundButton />
<SoundButton disabled className="my-sound-btn" />TimelineBar
Barra de reprodução padrão. Lê o TimelineProvider mais próximo (montado automaticamente dentro do ReelPlayerOverlay) e desenha a trilha, os trechos em buffer, o preenchimento do progresso e a pílula de arrasto. Personalize pelas propriedades --rk-reel-timeline-*, ou substitua por 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
A camada em gradiente padrão, que mostra autor, descrição e curtidas. Aparece sozinha quando o conteúdo tem os campos necess ários. Use renderSlideOverlay para substituí-la ou escondê-la.
import { SlideOverlay } from '@reelkit/react-reel-player';
<SlideOverlay
author={{ name: 'John', avatar: '/avatar.jpg' }}
description="Amazing content"
likes={12500}
/>ImageSlide
Slide de imagem com carregamento tardio e object-fit: cover por padrão. Use dentro de renderSlide para compor slides de imagem com os seus 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 com elemento <video> compartilhado para a continuidade do som no iOS, quadros de pôster, memória de posição e indicador de carregamento. Precisa estar dentro de um SoundProvider (que o ReelPlayerOverlay fornece sozinho).
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 }}
/>Compondo slides próprios
Use renderSlide com ImageSlide / VideoSlide para personalizar a exibição da mídia mantendo todo o comportamento embutido (reprodução automática, captura de pôster, sincronia do som).
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
}}
/>Carregamento de conteúdo e tratamento de erros
O player acompanha o carregamento e os erros de cada slide. Uma animação de onda aparece enquanto o conteúdo carrega; um ícone de erro aparece para mídias quebradas. As URLs com erro ficam em cache, então voltar a elas mostra o erro na hora, sem nova tentativa.
Callbacks de ciclo de vida
Ao usar renderSlide, chame estes callbacks para comandar o indicador de carregamento:
| Callback | Quando chamar |
|---|---|
onReady | A imagem carregou ou o vídeo começou a tocar. Limpa os estados de carregamento e erro. |
onWaiting | O vídeo está carregando no meio da reprodução. Mostra o indicador de carregamento. |
onError | O conteúdo não carregou. Mostra a camada de erro e registra a URL como quebrada. |
// 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>
)}Interface própria de carregamento e erro
Troque a animação de onda e o ícone de erro padrão por componentes seus:
<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>
)}
/>Linha do tempo
O overlay desenha uma barra de reprodução embutida sobre o vídeo ativo. A prop timeline, do tipo TimelineMode, libera ou não essa exibição:
'auto'(padrão): aparece quando a mídia ativa é um vídeo mais longo quetimelineMinDurationSeconds(30 por padrão). Vale para slides de vídeo único e para carrosséis de várias mídias; a barra segue o item aninhado ativo e some nas imagens.'always': aparece sempre que o slide ativo tiver vídeo.'never': nunca aparece. Monte uma barra sua comrenderTimeline.
<ReelPlayerOverlay
isOpen={isOpen}
onClose={close}
content={items}
timeline="auto"
timelineMinDurationSeconds={30}
/>Personalize pelas propriedades CSS --rk-reel-timeline-* (altura, cores, tamanho do cursor). Para uma barra, um cronômetro ou um indicador de progresso totalmente próprios, use renderTimeline; o callback recebe um timelineState apoiado no TimelineController por baixo.
Um componente renderizado dentro do overlay pode ler o mesmo controlador por conta própria: useTimelineState() o devolve a partir do TimelineProvider mais próximo (montado pelo overlay) e lança um erro fora de um; useTimelineStateOptional() devolve null nesse caso, para componentes que também podem renderizar fora do player.
Contexto de som
Em implementações próprias, você pode alcançar o estado do som:
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>
);
}Classes CSS
Todas as classes CSS são simples (não são CSS modules), então dá para alcançá-las com seletores de especificidade maior em uma folha de estilo carregada depois de @reelkit/react-reel-player/styles.css. Para mudar cor, tamanho e z-index, prefira as propriedades CSS documentadas na seção Temas, abaixo — elas existem exatamente para isso.
| Classe | Componente | Descrição |
|---|---|---|
.rk-reel-overlay | Overlay | Fundo fixo em tela cheia (cor de fundo, z-index) |
.rk-reel-container | Overlay | Contêiner do player (posição, transbordo) |
.rk-reel-loader | Overlay | Camada da animação de onda do carregamento |
.rk-reel-media-error | Overlay | Camada do estado de erro (ícone e texto centralizados) |
.rk-reel-media-error-text | Overlay | Texto da mensagem de erro |
.rk-reel-button | Controls | Botão circular de ícone compartilhado (fechar, som, setas) |
.rk-reel-close-btn | Controls | Botão de fechar |
.rk-reel-sound-btn | Controls | Botão de alternar o som |
.rk-reel-nav-arrows | Navigation | Contêiner das setas, só no desktop (escondido com 768px de largura ou menos) |
.rk-reel-nav-button | Navigation | Cada seta de avançar e voltar |
.rk-reel-slide-wrapper | Slide | Invólucro em volta da mídia e da camada sobreposta |
.rk-reel-slide-overlay | SlideOverlay | Contêiner da camada em gradiente |
.rk-reel-slide-overlay-author | SlideOverlay | Linha do autor (avatar e nome) |
.rk-reel-slide-overlay-avatar | SlideOverlay | Imagem do avatar do autor |
.rk-reel-slide-overlay-name | SlideOverlay | Texto do nome do autor |
.rk-reel-slide-overlay-description | SlideOverlay | Texto da descrição |
.rk-reel-slide-overlay-likes | SlideOverlay | Linha das curtidas (coração e contagem) |
.rk-reel-video-container | VideoSlide | Invólucro do vídeo (cor de fundo, transbordo) |
.rk-reel-video-element | VideoSlide | O elemento <video> |
.rk-reel-video-poster | VideoSlide | Imagem de pôster (some ao dar play) |
.rk-reel-video-poster.rk-visible | VideoSlide | Modificador de estado aplicado ao pôster enquanto o vídeo está pausado ou carregando |
.rk-reel-nested-indicator | NestedSlider | Pontinhos de paginação sob os slides com várias mídias (a posição muda entre desktop e toque) |
.rk-reel-nested-nav | NestedSlider | Setas do carrossel horizontal (escondidas com 768px de largura ou menos) |
.rk-reel-nested-nav-next | NestedSlider | Posição da seta de avançar aninhada |
.rk-reel-nested-nav-prev | NestedSlider | Posição da seta de voltar aninhada |
.rk-reel-timeline | TimelineBar | Invólucro da barra de arrasto. Reaproveite nas raízes de um `renderTimeline` próprio para herdar o posicionamento rente à base, o respiro da área segura e o espaço reservado para a camada do slide em dispositivos de toque. |
.rk-reel-timeline-track | TimelineBar | Trilha (trecho ainda não reproduzido) |
.rk-reel-timeline-buffered | TimelineBar | Camada dos trechos em buffer |
.rk-reel-timeline-fill | TimelineBar | Preenchimento do que já foi reproduzido |
.rk-reel-timeline-cursor | TimelineBar | Pílula da alça de arrasto (flutua acima da trilha) |
Temas
Cada cor, tamanho, z-index e transição vive em uma propriedade CSS. Sobrescreva uma ou várias em :root (ou em qualquer ancestral do overlay) para trocar o tema sem mexer no código dos componentes.
| Token | Padrão | O que controla |
|---|---|---|
--rk-reel-overlay-bg | #000 | Cor do fundo em tela cheia |
--rk-reel-overlay-z | 1000 | z-index do overlay |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Fundo padrão dos botões circulares |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Fundo das setas de navegação (e estado básico ao passar o mouse) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Fundo das setas ao passar o mouse |
--rk-reel-button-fg | #fff | Cor do ícone do botão |
--rk-reel-button-size | 44px | Largura e altura do botão |
--rk-reel-button-radius | 50% | Arredondamento do botão |
--rk-reel-ui-z | 10 | z-index de fechar, som e navegação |
--rk-reel-edge-padding | 16px | Recuo da borda para fechar, som e setas |
--rk-reel-nav-gap | 8px | Espaço entre as setas empilhadas |
--rk-reel-transition | 0.2s | Duração da transição ao passar o mouse |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Cor do gradiente da animação de onda |
--rk-reel-loader-duration | 1.8s | Duração da animação de onda |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Cor do ícone e do texto de erro |
--rk-reel-error-text-size | 13px | Tamanho da fonte da mensagem de erro |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Gradiente que escurece o fundo da legenda |
--rk-reel-slide-overlay-padding | 48px 16px 16px | Respiro interno da legenda |
--rk-reel-slide-overlay-name-color | #fff | Cor do nome do autor |
--rk-reel-slide-overlay-description-color | rgba(255, 255, 255, 0.9) | Cor do texto da descrição |
--rk-reel-slide-overlay-likes-color | rgba(255, 255, 255, 0.8) | Cor do texto da linha de curtidas |
--rk-reel-video-bg | #000 | Cor das faixas em volta do <video> |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Fundo das setas aninhadas |
--rk-reel-nested-button-bg-hover | rgba(255, 255, 255, 0.2) | Fundo das setas aninhadas ao passar o mouse |
--rk-reel-nested-button-size | 36px | Tamanho das setas aninhadas |
--rk-reel-nested-edge-padding | 12px | Recuo das setas aninhadas em relação à borda |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Fundo da trilha (trecho ainda não reproduzido) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Cor dos trechos em buffer |
--rk-reel-timeline-fill | #fff | Cor do preenchimento já reproduzido |
--rk-reel-timeline-cursor | #fff | Cor da pílula da alça de arrasto |
--rk-reel-timeline-height | 3px | Altura da trilha em repouso |
--rk-reel-timeline-height-active | 6px | Altura da trilha ao passar o mouse, no foco ou durante o arrasto |
--rk-reel-timeline-cursor-width | 10px | Largura da pílula em repouso |
--rk-reel-timeline-cursor-width-active | 14px | Largura da pílula durante o arrasto |
--rk-reel-timeline-cursor-height | 24px | Altura da pílula em repouso |
--rk-reel-timeline-cursor-height-active | 32px | Altura da pílula durante o arrasto |
--rk-reel-timeline-hitbox | 16px | Área extra de toque acima da trilha |
--rk-reel-timeline-transition | 0.15s ease-out | Animação de crescer e encolher da trilha e da pílula |
--rk-reel-timeline-z | 11 | z-index da linha do tempo (acima da camada padrão da interface) |
Cole o trecho abaixo em uma folha de estilo carregada depois 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;
}Acessibilidade
A raiz do overlay é um diálogo modal (role="dialog", aria-modal="true"). Defina ariaLabel para mudar o que o leitor de tela anuncia; o padrão é "Video player". Cada slide carrega role="group", aria-roledescription="slide" e um aria-label="Slide N of M", então deslizar anuncia a posição na sequência.
O overlay captura o foco ao abrir e o devolve ao gatilho ao fechar. Tab e Shift+Tab percorrem os elementos focáveis lá dentro; um foco que escapa (clique fora, foco por código) é trazido de volta. Implementado com captureFocusForReturn e createFocusTrap, de @reelkit/core.
Atalhos de teclado
| Tecla | Ação |
|---|---|
ArrowUp | Slide anterior |
ArrowDown | Próximo slide |
ArrowLeft | Mídia anterior (no slider aninhado) |
ArrowRight | Próxima mídia (no slider aninhado) |
Escape | Fecha o player |