Reel Player

Um player de vídeo em tela cheia no estilo Instagram Reels/TikTok, com @reelkit/react-reel-player.

Ver demonstração ao vivo →

Recursos

Deslize vertical
Toque, arrasto, teclado, roda
Vídeo automático
Toca quando fica visível
Botão de som
Continuidade no iOS
Várias mídias
Carrosséis horizontais aninhados
Memória de posição
Retoma de onde parou
Captura de quadro
Transição suave do pôster ao vídeo
Virtualizado
Apenas 3 slides no DOM
Proporção
9:16 no desktop, tela inteira no celular
Navegação no desktop
Botões de seta
Tipos genéricos
Modelos de dados próprios
Personalizável
Render props para tudo
Camada sobre o slide
Autor, curtidas, descrição
Estado na URL
Links para compartilhar e favoritar

Instalação

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

Não esqueça de importar os estilos:

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

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}
      />
    </>
  );
}
ReelPlayerPage.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: '/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:

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>
  )}
/>

Camada própria sobre o slide

Troque a camada padrão por um conteúdo seu em 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 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:

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 próprios

Componha os subcomponentes reutilizáveis com os seus acréscimos:

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>
    </>
  )}
/>

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.

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} />
  )}
/>

Navegação própria

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>
  )}
/>

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:

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

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

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} />

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

ChaveFioO que carrega
urlIndexKey(…)?reel=3Apenas o post vertical.
urlIndexTwoAxisKey(…)?reel=3.2O 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.

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} />

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:

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} />

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.

tsx
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á.

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

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
    },
  },
});
  • Enquanto locateAsync está pendente, o player fica fechado e o parâmetro é deixado em paz, então o link direto sobrevive à busca. Um null ou 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 null quando 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.

PropTipoPadrãoDescrição
apiRefMutableRefObject<ReelApi>-Ref para alcançar a API do Reel
ariaLabelstring'Video player'Rótulo acessível da região do diálogo; anunciado pelos leitores de tela quando o overlay abre
aspectRationumber9/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.
contentT[]obrigatórioArray de itens de conteúdo (genérico, ContentItem por padrão)
initialIndexnumber0Índice do slide inicial
initialInnerIndexnumber0Í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.
isOpenbooleanobrigatórioControla 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).
timelineMinDurationSecondsnumber30Duraçã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.

PropTipoPadrãoDescrição
controllerUrlStateControllerobrigatórioControlador 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

PropTipoDescrição
onClose() => voidChamado 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) => voidChamado depois da troca de slide
onInnerSlideChange(outerIndex: number, innerIndex: number) => voidChamado 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.

PropTipoPadrãoDescrição
enableNavKeysbooleantrueLiga a navegação por teclado
enableWheelbooleantrueLiga a navegação pela roda do mouse
loopbooleanfalseLiga o ciclo infinito
swipeDistanceFactornumber0.12Limiar do deslize (0-1)
transitionDurationnumber300Duração da animação de transição (ms)
wheelDebounceMsnumber200Debounce 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.

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

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.

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ão de som isolado. Precisa estar dentro de um SoundProvider (que o ReelPlayerOverlay fornece sozinho).

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

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

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.

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

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

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 }}
/>
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).

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
  }}
/>

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:

CallbackQuando chamar
onReadyA imagem carregou ou o vídeo começou a tocar. Limpa os estados de carregamento e erro.
onWaitingO vídeo está carregando no meio da reprodução. Mostra o indicador de carregamento.
onErrorO conteúdo não carregou. Mostra a camada de erro e registra a URL como quebrada.
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>
)}

Interface própria de carregamento e erro

Troque a animação de onda e o ícone de erro padrão por componentes seus:

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>
  )}
/>

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 que timelineMinDurationSeconds (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 com renderTimeline.
tsx
<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:

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>
  );
}

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.

ClasseComponenteDescrição
.rk-reel-overlayOverlayFundo fixo em tela cheia (cor de fundo, z-index)
.rk-reel-containerOverlayContêiner do player (posição, transbordo)
.rk-reel-loaderOverlayCamada da animação de onda do carregamento
.rk-reel-media-errorOverlayCamada do estado de erro (ícone e texto centralizados)
.rk-reel-media-error-textOverlayTexto da mensagem de erro
.rk-reel-buttonControlsBotão circular de ícone compartilhado (fechar, som, setas)
.rk-reel-close-btnControlsBotão de fechar
.rk-reel-sound-btnControlsBotão de alternar o som
.rk-reel-nav-arrowsNavigationContêiner das setas, só no desktop (escondido com 768px de largura ou menos)
.rk-reel-nav-buttonNavigationCada seta de avançar e voltar
.rk-reel-slide-wrapperSlideInvólucro em volta da mídia e da camada sobreposta
.rk-reel-slide-overlaySlideOverlayContêiner da camada em gradiente
.rk-reel-slide-overlay-authorSlideOverlayLinha do autor (avatar e nome)
.rk-reel-slide-overlay-avatarSlideOverlayImagem do avatar do autor
.rk-reel-slide-overlay-nameSlideOverlayTexto do nome do autor
.rk-reel-slide-overlay-descriptionSlideOverlayTexto da descrição
.rk-reel-slide-overlay-likesSlideOverlayLinha das curtidas (coração e contagem)
.rk-reel-video-containerVideoSlideInvólucro do vídeo (cor de fundo, transbordo)
.rk-reel-video-elementVideoSlideO elemento <video>
.rk-reel-video-posterVideoSlideImagem de pôster (some ao dar play)
.rk-reel-video-poster.rk-visibleVideoSlideModificador de estado aplicado ao pôster enquanto o vídeo está pausado ou carregando
.rk-reel-nested-indicatorNestedSliderPontinhos de paginação sob os slides com várias mídias (a posição muda entre desktop e toque)
.rk-reel-nested-navNestedSliderSetas do carrossel horizontal (escondidas com 768px de largura ou menos)
.rk-reel-nested-nav-nextNestedSliderPosição da seta de avançar aninhada
.rk-reel-nested-nav-prevNestedSliderPosição da seta de voltar aninhada
.rk-reel-timelineTimelineBarInvó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-trackTimelineBarTrilha (trecho ainda não reproduzido)
.rk-reel-timeline-bufferedTimelineBarCamada dos trechos em buffer
.rk-reel-timeline-fillTimelineBarPreenchimento do que já foi reproduzido
.rk-reel-timeline-cursorTimelineBarPí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.

TokenPadrãoO que controla
--rk-reel-overlay-bg#000Cor do fundo em tela cheia
--rk-reel-overlay-z1000z-index do overlay
--rk-reel-button-bgrgba(0, 0, 0, 0.5)Fundo padrão dos botões circulares
--rk-reel-button-bg-hoverrgba(255, 255, 255, 0.1)Fundo das setas de navegação (e estado básico ao passar o mouse)
--rk-reel-button-bg-hover-strongrgba(255, 255, 255, 0.2)Fundo das setas ao passar o mouse
--rk-reel-button-fg#fffCor do ícone do botão
--rk-reel-button-size44pxLargura e altura do botão
--rk-reel-button-radius50%Arredondamento do botão
--rk-reel-ui-z10z-index de fechar, som e navegação
--rk-reel-edge-padding16pxRecuo da borda para fechar, som e setas
--rk-reel-nav-gap8pxEspaço entre as setas empilhadas
--rk-reel-transition0.2sDuração da transição ao passar o mouse
--rk-reel-loader-colorrgba(255, 255, 255, 0.12)Cor do gradiente da animação de onda
--rk-reel-loader-duration1.8sDuração da animação de onda
--rk-reel-error-fgrgba(255, 255, 255, 0.4)Cor do ícone e do texto de erro
--rk-reel-error-text-size13pxTamanho da fonte da mensagem de erro
--rk-reel-slide-overlay-bglinear-gradient(transparent, rgba(0, 0, 0, 0.7))Gradiente que escurece o fundo da legenda
--rk-reel-slide-overlay-padding48px 16px 16pxRespiro interno da legenda
--rk-reel-slide-overlay-name-color#fffCor do nome do autor
--rk-reel-slide-overlay-description-colorrgba(255, 255, 255, 0.9)Cor do texto da descrição
--rk-reel-slide-overlay-likes-colorrgba(255, 255, 255, 0.8)Cor do texto da linha de curtidas
--rk-reel-video-bg#000Cor das faixas em volta do <video>
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)Fundo das setas aninhadas
--rk-reel-nested-button-bg-hoverrgba(255, 255, 255, 0.2)Fundo das setas aninhadas ao passar o mouse
--rk-reel-nested-button-size36pxTamanho das setas aninhadas
--rk-reel-nested-edge-padding12pxRecuo das setas aninhadas em relação à borda
--rk-reel-timeline-trackrgba(255, 255, 255, 0.22)Fundo da trilha (trecho ainda não reproduzido)
--rk-reel-timeline-bufferedrgba(255, 255, 255, 0.4)Cor dos trechos em buffer
--rk-reel-timeline-fill#fffCor do preenchimento já reproduzido
--rk-reel-timeline-cursor#fffCor da pílula da alça de arrasto
--rk-reel-timeline-height3pxAltura da trilha em repouso
--rk-reel-timeline-height-active6pxAltura da trilha ao passar o mouse, no foco ou durante o arrasto
--rk-reel-timeline-cursor-width10pxLargura da pílula em repouso
--rk-reel-timeline-cursor-width-active14pxLargura da pílula durante o arrasto
--rk-reel-timeline-cursor-height24pxAltura da pílula em repouso
--rk-reel-timeline-cursor-height-active32pxAltura da pílula durante o arrasto
--rk-reel-timeline-hitbox16pxÁrea extra de toque acima da trilha
--rk-reel-timeline-transition0.15s ease-outAnimação de crescer e encolher da trilha e da pílula
--rk-reel-timeline-z11z-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.

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

TeclaAção
ArrowUpSlide anterior
ArrowDownPróximo slide
ArrowLeftMídia anterior (no slider aninhado)
ArrowRightPróxima mídia (no slider aninhado)
EscapeFecha o player