Stories Player

Um player de stories em tela cheia no estilo Instagram para React, com @reelkit/react-stories-player.

Ver demonstração ao vivo →

Recursos

Navegação aninhada
Toque para avançar os stories, deslize para trocar de grupo
Stories em vídeo
Tocam sozinhos, com botão de som
Avanço automático
Tempo configurável por story
Transições 3D
Cubo, virada, esmaecer, aproximar, deslizar
Barra de progresso
Progresso segmentado desenhado em canvas
Imagem e vídeo
Dá conta dos dois tipos de mídia
Virtualizado
Apenas 3 slides no DOM
Curtida no toque duplo
Animação de coração no toque duplo
Navegação no desktop
Botões de seta no desktop
Anéis de story
Anéis em volta do avatar, como no Instagram
Tipos genéricos
Estenda StoryItem com dados próprios
Render Props
Personalize cada elemento da interface
Estado na URL
Links ?story=grupo.story para compartilhar
Itens já vistos
Anéis vistos e retomada sobrevivem ao recarregamento

Instalação

bash

Não esqueça de importar os estilos:

typescript
Ícones

O cabeçalho padrão usa lucide-react para os ícones. Se você prefere outra biblioteca de ícones, use renderHeader e renderNavigation para fornecer os seus.

Início rápido

O componente StoriesOverlay renderiza um player de stories em tela cheia. Combine-o com StoriesRingList para ter pontos de entrada no estilo Instagram. Passe um array de objetos StoriesGroup e controle a visibilidade com isOpen.

tsx

Demonstração ao vivo

StoriesPlayer.tsx
Alice
Alice
Bob
Bob
Charlie
Charlie

Clique em um anel para abrir o player. Toque nos lados esquerdo e direito para navegar, deslize para trocar de pessoa.

Estado na URL

Ver demonstração ao vivo →

StoriesUrlOverlay é um componente à parte cujo estado aberto vive na barra de endereços. Os dois eixos viajam em um único parâmetro — ?story=<grupo>.<story> — então o story em exibição ganha um link que dá para compartilhar, favoritar e fechar com o botão voltar. Monte um controlador com useOverlayUrlState e urlIndexTwoAxisKey e entregue-o como controller.

Chaves prontas

Stories têm dois eixos, então espalhe uma chave de dois eixos no controlador: urlIndexTwoAxisKey (grupo e story por posição) ou urlStableIdTwoAxisKey (o grupo por um id estável) — as duas reexportadas por @reelkit/react. Veja o guia de estado na URL e a API do core.

tsx
  • Abrir empilha uma entrada no histórico. Passar de story e trocar de pessoa substituem essa entrada, então N navegações não acrescentam nenhuma e um único passo para trás sempre fecha o player. Voltar fecha; não retrocede stories.
  • A navegação interna vai junto. O índice do story não fica congelado na granularidade do grupo — avançar dentro dos stories de uma pessoa atualiza ?story=2.n, então um link direto cai no story exato.
  • 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 parâmetro que não nomeia grupo nem story — um favorito velho, um valor editado à mão, um story além do fim de um grupo — é retirado da URL, em vez de abrir um vizinho.

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

Links estáveis. Por padrão o grupo é posicional, então um ?story=2.0 favoritado abre outra pessoa assim que o feed muda de ordem. Enderece o grupo por um id estável — outerCodec escreve o id na URL, outerLocator descobre onde ele está. A metade do story segue sendo um índice simples dentro do grupo resolvido.

tsx

Feeds infinitos. Paginar é assunto do outerLocator, independente do codec. locate é síncrono, então só responde pelos grupos já carregados — um link compartilhado para o grupo 400 de um feed que carregou 20 volta vazio. locateAsync é o plano B, chamado somente quando locate falha; o story é então limitado de novo pelo grupo em que a busca parar.

O mesmo locateAsync, no eixo externo

Este é o mesmo paginador locateAsync das chaves de eixo único — em uma chave de dois eixos ele viaja no outerLocator que você passa, então o eixo dos grupos pagina enquanto o story continua um índice local dentro do grupo resolvido.

tsx
  • 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 story que ninguém pediu.
  • As opções completas de useOverlayUrlState estão na referência da API para React, e o passo a passo está no guia para React.

Lembrar o que já foi visto

Os anéis exibem o gradiente até o grupo ser assistido até o fim, e um grupo reabre no primeiro story que o leitor ainda não viu. As duas coisas vêm de um ViewedStateController, montado com a mesma chave da barra de endereços — então a entrada guardada fica idêntica ao parâmetro de um link compartilhado e sobrevive à reordenação do feed quando a chave é endereçada por id.

tsx
  • O story de abertura conta. O story já na tela é registrado como visto na montagem, então abrir um grupo de um story só e fechar já o marca como assistido.
  • Deslizar adiante também retoma. resumeStoryIndex é consultado para todo grupo alcançado pela primeira vez na sessão, inclusive aquele em que o player abriu, a menos que initialStoryIndex nomeie um story diretamente; um grupo já percorrido reabre onde parou.
  • O link ainda vence. Com StoriesUrlOverlay, quem decide onde o player abre é o parâmetro, independentemente do que estiver guardado. Em todos os outros casos quem decide é o callback de retomada.
  • A contagem é uma posição, não um total. A entrada nomeia o story mais distante alcançado, então acrescentar um story a um grupo já assistido acende o anel de novo, e remover um do meio encurta a contagem. A mesma cura espontânea que um link compartilhado recebe.
  • O armazenamento é trocável. Passe createSessionStorageAdapter() para esquecer ao fechar a aba, ou o seu próprio StorageAdapter. Duas abas abertas ficam em sintonia pelo evento de armazenamento do navegador.

Referência da API

StoriesOverlayProps

StoriesOverlayProps<T>

PropTipoPadrãoDescrição
isOpenbooleanobrigatórioControla a visibilidade do overlay. Com true, a rolagem do corpo fica travada.
groupsStoriesGroup<T>[]obrigatórioArray de grupos de stories a exibir
onClose() => voidobrigatórioCallback para fechar o overlay
ariaLabelstring'Stories player'Rótulo acessível da região do diálogo; anunciado pelos leitores de tela quando o overlay abre
initialGroupIndexnumber0Índice, começando em zero, do grupo visível no início
initialStoryIndexnumberresumeStoryIndex(initialGroupIndex), senão 0Índice, começando em zero, do story visível no início dentro do grupo. Informar um valor vence qualquer coisa lembrada; deixe de fora e o grupo de abertura retoma como todos os outros.
resumeStoryIndex(groupIndex: number) => numberEm qual story o grupo abre na primeira vez que é alcançado, para que o leitor continue de onde parou. Só é consultado para um grupo ainda não visitado nesta abertura.
groupTransitionTransitionTransformFncubeTransitionEfeito de transição do slider externo (o dos grupos)
defaultImageDurationnumber5000Duração padrão, em milissegundos, do avanço automático dos stories de imagem
tapZoneSplitnumber0.3Proporção que divide as zonas de toque (0–1). A parte esquerda volta, a direita avança.
hideUIOnPausebooleantrueSe a interface do story (cabeçalho, rodapé) some ao pausar com um toque longo
enableKeyboardbooleantrueLiga a navegação por teclado (setas esquerda e direita, Escape)
innerTransitionDurationnumber200Duração, em milissegundos, da animação de transição interna (entre stories)
minSegmentWidthnumber8Largura mínima, em pixels, de um segmento da barra de progresso
apiRefMutableRefObject<StoriesApi | null>-Ref para alcançar a StoriesApi imperativa
renderHeader(props: HeaderRenderProps<T>) => ReactNode-Renderizador próprio do cabeçalho. Recebe autor, story e os estados de pausa e de som.
renderFooter(props: FooterRenderProps<T>) => ReactNode-Renderizador próprio do rodapé. Recebe os dados do autor e do story.
renderSlide(props: SlideRenderProps<T>) => ReactNode-Renderizador próprio do slide, no lugar dos slides padrão de imagem e vídeo.
renderNavigation(props: NavigationRenderProps) => ReactNode-Navegação própria no desktop. Substitui os botões padrão de avançar e voltar.
renderProgressBar(props: ProgressBarRenderProps<T>) => ReactNode-Barra de progresso própria. Substitui a barra padrão desenhada em canvas.
renderLoading(props: LoadingRenderProps<T>) => ReactNode-Renderizador próprio do carregamento. Sem ele, aparece o girador padrão no cabeçalho.
renderError(props: ErrorRenderProps<T>) => ReactNode-Renderizador próprio do erro. Sem ele, aparece a camada padrão com o ícone de erro.

StoriesUrlOverlayProps

StoriesUrlOverlayProps<T>

Aceita todas as props do StoriesOverlay, menos o trio do estado aberto — isOpen, initialGroupIndex, initialStoryIndex —, que vem do controlador.

PropTipoPadrãoDescrição
controllerUrlStateController<TwoAxisPosition>obrigatórioControlador vindo de useOverlayUrlState, espalhado com urlIndexTwoAxisKey. A posição dele — um objeto { outer, inner } — decide se o player está aberto e onde abre; o overlay escreve de volta a cada navegação e no fechamento.

Callbacks

PropTipoDescrição
onClose() => voidChamado quando o player fecha. Obrigatório no StoriesOverlay (o estado aberto é seu, então o fechamento também); opcional no StoriesUrlOverlay, onde quem fecha é a URL — passe apenas se quiser reagir depois do fechamento.
onStoryChange(groupIndex: number, storyIndex: number) => voidDisparado quando o story ativo muda
onGroupChange(groupIndex: number) => voidDisparado quando o grupo ativo muda
onStoryViewed(groupIndex: number, storyIndex: number) => voidDisparado quando um story fica visível
onStoryComplete(groupIndex: number, storyIndex: number) => voidDisparado quando o timer do story termina
onDoubleTap(groupIndex: number, storyIndex: number) => voidDisparado no gesto de toque duplo
onPause() => voidDisparado quando o player é pausado
onResume() => voidDisparado quando o player é retomado

Transições

A prop groupTransition controla o efeito 3D ao deslizar entre os grupos de pessoas. Importe as funções de transição de @reelkit/react:

tsx

Ciclo de vida do carregamento de conteúdo

Cada slide de story informa seu estado de carregamento pelos callbacks que chegam em SlideRenderProps:

CallbackQuando
onReadyO conteúdo está pronto (imagem carregada, vídeo tocando). O timer de progresso começa.
onWaitingO conteúdo travou (vídeo carregando no meio da reprodução). O girador aparece e o timer pausa.
onErrorO conteúdo não carregou. A camada de erro aparece.
onDurationReadyInforme a duração real da mídia (vinda dos metadados do vídeo, por exemplo) para reiniciar o timer com o tempo certo.
onEndedAvise que a mídia terminou (o vídeo acabou, por exemplo). Avança para o próximo story.
Cache do pré-carregador

Os componentes ImageStorySlide e VideoStorySlide que já vêm prontos pré-carregam o próximo story em segundo plano. Quando o leitor chega a um story pré-carregado, o conteúdo aparece na hora, sem girador.

Render Props

Todo elemento da interface pode ser substituído por uma render prop. Cada uma recebe props tipadas com o estado e os callbacks necessários.

renderHeader

Substitua o cabeçalho padrão (dados do autor, botões de pausa e som, botão de fechar):

tsx

renderFooter

Acrescente um rodapé abaixo do conteúdo do story:

tsx

renderSlide

Substitua por completo os slides padrão de imagem e vídeo. Use os subcomponentes ImageStorySlide e VideoStorySlide para aproveitar o tratamento de mídia que já vem pronto:

tsx

renderNavigation

Substitua os botões de seta padrão do desktop:

tsx

renderProgressBar

Substitua a barra de progresso padrão em canvas por uma implementação sua. O sinal progress emite valores de 0 a 1:

tsx

renderLoading

Indicador de carregamento próprio enquanto o conteúdo é buscado:

tsx

renderError

Camada de erro própria para quando o conteúdo não carrega:

tsx

StoriesApi

Use a prop apiRef para o controle imperativo:

tsx

Métodos

MétodoTipoDescrição
nextStory()() => voidAvança para o próximo story dentro do grupo atual
prevStory()() => voidVolta ao story anterior dentro do grupo atual
nextGroup()() => voidMuda para o próximo grupo
prevGroup()() => voidMuda para o grupo anterior
goToGroup(index)(index: number) => voidSalta direto para um grupo pelo índice
pause()() => voidPausa o avanço automático e o timer de progresso
resume()() => voidRetoma o avanço automático e o timer de progresso

Toque duplo e curtidas

Uma animação de coração já embutida toca no toque duplo, dando um retorno visual imediato. O callback onDoubleTap dispara com os índices do grupo e do story, então você guarda a curtida no seu próprio estado (chamada de API, armazenamento local, o que for). O player não guarda esse estado internamente.

tsx

Personalizando a animação do coração

Ajuste a velocidade da animação pelo token --rk-stories-heart-duration (veja Temas). Para mudar cor, tamanho ou esconder o coração por completo, mire direto na classe .rk-stories-heart. O componente HeartAnimation também é exportado para uso isolado.

css

Por ora, a animação de coração embutida não pode ser trocada por uma render prop. Você pode reestilizá-la no CSS ou escondê-la com display: none e cuidar da sua própria animação no callback onDoubleTap. Se você precisa de uma render prop renderDoubleTap, conte para a gente nas issues do GitHub.

Subcomponentes

Blocos reutilizáveis exportados para você compor dentro das suas render props:

CanvasProgressBar

Barra de progresso segmentada em canvas, de alto desempenho. Desenha um segmento por story e anima o preenchimento do segmento ativo com requestAnimationFrame. Aceita uma janela deslizante para grupos com muitos stories.

tsx

StoryHeader

Cabeçalho padrão, com avatar e nome do autor, selo de verificado, horário relativo, botão de pausa, botão de som, girador de carregamento e botão de fechar. Usado automaticamente quando renderHeader não é informado.

tsx

ImageStorySlide

Slide de imagem de borda a borda, com object-fit: cover. Informa carregamento e erro pelos callbacks do ciclo de vida.

tsx

VideoStorySlide

Slide de vídeo que usa um elemento <video> compartilhado para a continuidade do som no iOS. Cuida da reprodução automática, dos quadros de pôster e da sincronia do som, e informa a duração e os eventos do ciclo de reprodução.

tsx

StoriesRing

Avatar circular com anel em gradiente no estilo Instagram. Dois estados: um grupo com stories por assistir ganha o gradiente giratório, um totalmente assistido ganha um anel discreto e parado.

tsx

StoriesRingList

Fileira horizontal e rolável de componentes StoriesRing, com o nome de cada autor. Um anel por grupo.

tsx

HeartAnimation

Camada animada de coração, disparada no toque duplo. Cresce e some ao longo de 800ms. Personalize pelo CSS (veja a seção Toque duplo e curtidas).

tsx

Tipos

StoryItem

typescript

AuthorInfo

typescript

StoriesGroup<T>

typescript

HeaderRenderProps<T>

typescript

FooterRenderProps<T>

typescript

SlideRenderProps<T>

typescript
typescript

ProgressBarRenderProps<T>

typescript

LoadingRenderProps<T>

typescript

ErrorRenderProps<T>

typescript

StoriesApi

typescript

Tipos de Story personalizados

Estenda StoryItem com campos seus e passe o parâmetro de tipo ao StoriesOverlay. Todas as render props vão receber o seu tipo estendido:

tsx

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-stories-player/styles.css. Para mudar cor, tamanho e z-index, prefira as propriedades CSS documentadas na seção Temas, abaixo.

ClasseComponenteDescrição
.rk-stories-overlayOverlayFundo fixo em tela cheia (cor de fundo, z-index)
.rk-stories-swipe-wrapperOverlayInvólucro do deslizar para fechar (abriga os botões de navegação e o canvas)
.rk-stories-containerOverlayÁrea arredondada do story (posição, transbordo)
.rk-stories-ui-layerOverlayContêiner da interface sobreposta (cabeçalho, progresso, navegação)
.rk-stories-ui-layer--hiddenOverlayEstado de interface escondida (alternado por hideUIOnPause)
.rk-stories-errorOverlayEstado de erro (ícone e texto centralizados)
.rk-stories-error-textOverlayTexto da mensagem de erro
.rk-stories-nav-btnNavigationSeta de avançar e voltar no desktop
.rk-stories-progress-barProgressBarInvólucro que posiciona a barra de progresso em canvas
.rk-stories-slide-wrapperGroupUm grupo de stories (slide externo)
.rk-stories-storyStoryUm story (raiz do slide interno)
.rk-stories-headerStoryHeaderBarra do cabeçalho (avatar, nome, ações)
.rk-stories-header--hiddenStoryHeaderEstado de cabeçalho escondido (visible=false)
.rk-stories-header-avatarStoryHeaderImagem do avatar do autor
.rk-stories-header-nameStoryHeaderTexto do nome do autor
.rk-stories-header-verifiedStoryHeaderContêiner do selo de verificado
.rk-stories-header-timeStoryHeaderTexto do "há quanto tempo"
.rk-stories-header-actionsStoryHeaderAções do lado direito (fechar, som, pausa)
.rk-stories-header-btnStoryHeaderBotão de ação do cabeçalho
.rk-stories-header-spinnerStoryHeaderGirador do vídeo carregando
.rk-stories-imageImageStorySlideElemento do story de imagem
.rk-stories-videoVideoStorySlideContêiner do story de vídeo
.rk-stories-video-elementVideoStorySlideO elemento <video> compartilhado
.rk-stories-video-posterVideoStorySlideImagem de pôster do vídeo (some ao dar play)
.rk-stories-video-poster--visibleVideoStorySlideEstado de pôster visível (antes da reprodução)
.rk-stories-heartHeartAnimationAnimação do coração que salta no toque duplo
.rk-stories-ringStoriesRingAnel do story (avatar com borda em gradiente animado)
.rk-stories-ring--activeStoriesRingAnel com stories por assistir (anima)
.rk-stories-ring-avatarStoriesRingImagem do avatar dentro do anel
.rk-stories-ring-listStoriesRingListContêiner da fileira horizontal de anéis
.rk-stories-ring-list-itemStoriesRingListColuna com o anel e o nome
.rk-stories-ring-list-nameStoriesRingListNome do autor abaixo de cada anel

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-stories-overlay-bg#000Cor do fundo em tela cheia
--rk-stories-overlay-z9999z-index do overlay
--rk-stories-container-radius12pxArredondamento dos cantos da área do story (desktop)
--rk-stories-swipe-gap16pxEspaço entre os botões de navegação e a área do story
--rk-stories-top-shade-height120pxAltura do gradiente atrás do cabeçalho
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)Cor do gradiente do topo
--rk-stories-ui-transition200msDuração do esmaecimento quando hideUIOnPause age
--rk-stories-nav-size44pxTamanho dos botões de avançar e voltar no desktop
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)Fundo dos botões de navegação no desktop
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)Fundo desses botões ao passar o mouse
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)Cor do ícone desses botões
--rk-stories-nav-fg-hover#fffCor do ícone ao passar o mouse
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)Gradiente de fundo do estado de erro
--rk-stories-error-fgrgba(255, 255, 255, 0.5)Cor do ícone e do texto de erro
--rk-stories-error-text-size13pxTamanho da fonte da mensagem de erro
--rk-stories-video-bg#000Cor das faixas em volta do <video>
--rk-stories-video-poster-transition200msDuração do esmaecimento do pôster quando o vídeo começa
--rk-stories-header-top18pxDistância do cabeçalho até o topo do story
--rk-stories-header-padding12px 16pxRespiro interno da linha do cabeçalho
--rk-stories-header-avatar-size32pxLargura e altura do avatar
--rk-stories-header-name-fg#fffCor do nome do autor
--rk-stories-header-name-size14pxTamanho da fonte do nome do autor
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)Cor do texto do "há quanto tempo"
--rk-stories-header-btn-fg#fffCor dos ícones de ação do cabeçalho (fechar, som, pausa)
--rk-stories-heart-duration800msDuração da animação do coração, do salto ao desaparecimento
--rk-stories-ring-spin-duration4sDuração de uma volta do anel com stories por assistir
--rk-stories-ring-list-gap12pxEspaço entre os anéis da fileira
--rk-stories-ring-list-padding12pxRespiro interno em volta da fileira de anéis
--rk-stories-ring-list-name-size12pxTamanho da fonte do nome abaixo de cada anel

Cole o trecho abaixo em uma folha de estilo carregada depois de @reelkit/react-stories-player/styles.css.

css

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 é "Stories player".

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
ArrowLeftStory anterior
ArrowRightPróximo story
EscapeFecha o player