Vue Reel Player

Player vertical de mídia em tela cheia no estilo Instagram/TikTok para Vue 3, construído sobre @reelkit/vue-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
Slots com escopo
Personalize cada elemento da interface
v-model:is-open
Ligação de mão dupla na visibilidade
Estado na URL
Links para compartilhar, fechamento no botão voltar

Instalação

bash
npm install @reelkit/vue-reel-player @reelkit/vue lucide-vue-next

Importe a folha de estilo uma vez na entrada do seu app (ou em qualquer componente):

typescript
import '@reelkit/vue-reel-player/styles.css';
Ícones

Os controles padrão usam lucide-vue-next para os ícones (fechar, som, setas de navegação). Se você prefere outra biblioteca de ícones, use os slots com escopo #controls e #navigation para fornecer os seus.

Vue 3.3 ou mais recente

Os overlays são componentes genéricos, que o Vue suporta a partir da 3.3. Num Vue mais antigo o runtime descarta as props e os eventos deles, por isso o pacote exige vue 3.3 ou mais recente como peer dependency.

Início rápido

Desenhe uma grade de miniaturas e abra o overlay no índice clicado. Ligar v-model:is-open mantém o ref do componente pai em sintonia quando o leitor fecha o player pelo botão, por gesto ou com Escape.

App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import '@reelkit/vue-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-02.mp4',
      poster: '/cdn/samples/videos/video-poster-02.jpg',
      aspectRatio: 16 / 9,
    }],
    author: { name: 'Mike Chen', avatar: '/cdn/samples/avatars/avatar-03.jpg' },
    likes: 3456,
    description: 'Adventure awaits',
  },
];

const isOpen = ref(false);
const startIndex = ref(0);

function openAt(i: number) {
  startIndex.value = i;
  isOpen.value = true;
}
</script>

<template>
  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
    <button
      v-for="(item, i) in content"
      :key="item.id"
      @click="openAt(i)"
      style="aspect-ratio:9/16;cursor:pointer;overflow:hidden;padding:0;border:0"
    >
      <img
        :src="item.media[0].poster || item.media[0].src"
        style="width:100%;height:100%;object-fit:cover"
      />
    </button>
  </div>

  <ReelPlayerOverlay
    v-model:is-open="isOpen"
    :content="content"
    :initial-index="startIndex"
  />
</template>

Slots com escopo

Oito slots com escopo permitem trocar qualquer parte da interface do player. Cada um recebe um objeto de escopo fortemente tipado. Os slots que você não passar caem no padrão.

SlotEscopoDescrição
#controls{ item, soundState, activeIndex, content, onClose }Barra de controles global própria (fechar, som, compartilhar e afins)
#error{ item, activeIndex, innerActiveIndex }Indicador de erro próprio (substitui o ícone padrão)
#loading{ item, activeIndex, innerActiveIndex }Indicador de carregamento próprio (substitui a animação padrão de onda)
#navigation{ item, activeIndex, count, onPrev, onNext }Setas próprias de avançar e voltar (desktop)
#nestedNavigation{ media, activeIndex, count, onPrev, onNext }Setas próprias do slider horizontal interno
#nestedSlide{ item, media, index, size, isActive, isInnerActive, slideKey, defaultContent, onReady, onWaiting, onError }Conteúdo próprio dos slides dentro do slider horizontal interno
#slide{ item, index, size, isActive, slideKey, defaultContent, onReady, onWaiting, onError }Conteúdo de slide totalmente seu (sem ele, vale o padrão)
#slideOverlay{ item, index, isActive }Camada de cada slide (dados do autor, curtidas, descrição e afins)
#timeline{ item, activeIndex, timelineState, defaultContent }Barra de reprodução própria. Só é chamada quando a regra embutida (modo da linha do tempo e duração mínima) renderizaria a barra padrão; reaproveita a mesma lógica de auto/always/never. Use defaultContent() para envolver a <TimelineBar /> embutida.
vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  ReelPlayerOverlay,
  CloseButton,
  SoundButton,
  type ContentItem,
  type SlideOverlaySlotScope,
  type ControlsSlotScope,
} from '@reelkit/vue-reel-player';

const isOpen = ref(false);
const content = ref<ContentItem[]>([/* ... */]);
</script>

<template>
  <ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
    <!-- Custom per-slide overlay: branded caption -->
    <template #slideOverlay="{ item, isActive }: SlideOverlaySlotScope">
      <div v-if="isActive" style="position:absolute;bottom:80px;left:16px;color:#fff">
        <div style="display:flex;align-items:center;gap:8px">
          <img :src="item.author.avatar" style="width:40px;height:40px;border-radius:50%" />
          <span style="font-weight:600">{{ item.author.name }}</span>
        </div>
        <p style="margin-top:8px">{{ item.description }}</p>
      </div>
    </template>

    <!-- Custom global controls -->
    <template #controls="{ onClose }: ControlsSlotScope">
      <div style="position:absolute;top:16px;right:16px;display:flex;gap:8px">
        <SoundButton />
        <CloseButton :on-click="onClose" />
      </div>
    </template>

    <!-- Custom playback timeline -->
    <template #timeline="{ timelineState }: TimelineSlotScope">
      <CustomTimelineBar :state="timelineState" />
    </template>
  </ReelPlayerOverlay>
</template>

Linha do tempo própria

Troque a barra de reprodução padrão pela sua, com o slot #timeline. O slot só dispara quando as regras do overlay renderizariam a barra padrão (o mesmo modo timeline e o mesmo 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 em dispositivos de toque.

vue
<script setup lang="ts">
import { shallowRef, onMounted, onBeforeUnmount } from 'vue';
import {
  ReelPlayerOverlay,
  type TimelineSlotScope,
} from '@reelkit/vue-reel-player';
import { toVueRef, type TimelineController } from '@reelkit/vue';

const trackRef = shallowRef<HTMLDivElement | null>(null);
let dispose: (() => void) | null = null;
const bind = (state: TimelineController) => {
  if (trackRef.value) dispose = state.bindInteractions(trackRef.value);
};
onBeforeUnmount(() => dispose?.());
</script>

<template>
  <ReelPlayerOverlay :is-open="open" :content="items" timeline="always">
    <template #timeline="{ timelineState }: TimelineSlotScope">
      <div class="rk-reel-timeline" style="padding: 0 16px" @vue:mounted="bind(timelineState)">
        <div ref="trackRef" role="slider" style="height:6px;background:rgba(255,255,255,0.2)">
          <div :style="{
            width: (timelineState.progress.value * 100) + '%',
            height: '100%',
            background: 'linear-gradient(90deg, #6366f1, #ec4899)',
          }" />
        </div>
      </div>
    </template>
  </ReelPlayerOverlay>
</template>

Tipos de conteúdo próprios

ReelPlayerOverlay e ReelPlayerUrlOverlay são genéricos em relação ao formato dos seus itens de conteúdo, como o player React. Estenda BaseContentItem para usar qualquer modelo de dados: o tipo do item é inferido de content, então um feed que não corresponde a BaseContentItem falha na checagem de tipos. No Vue os slots não conseguem carregar esse tipo, então os escopos ficam sem tipo até você anotá-los. Importe o tipo de escopo de slot correspondente e anote a desestruturação para os campos extras continuarem tipados dentro do template. Nada confere a anotação com content, então mantenha os dois em sintonia:

vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  ReelPlayerOverlay,
  type BaseContentItem,
  type SlideOverlaySlotScope,
} from '@reelkit/vue-reel-player';

interface MyItem extends BaseContentItem {
  title: string;
  category: 'video' | 'photo';
}

const open = ref(false);
const items: MyItem[] = [/* ... */];
</script>

<template>
  <ReelPlayerOverlay v-model:is-open="open" :content="items">
    <template #slideOverlay="{ item }: SlideOverlaySlotScope<MyItem>">
      <div class="my-overlay">
        <h2>{{ item.title }}</h2>
        <span>{{ item.category }}</span>
      </div>
    </template>
  </ReelPlayerOverlay>
</template>

O mesmo padrão vale para todos os outros slots. Importe o tipo de escopo correspondente (SlideSlotScope, ControlsSlotScope, NavigationSlotScope, NestedSlideSlotScope, LoadingSlotScope) e anote a desestruturação.

Estado na URL

Ver demonstração ao vivo →

Monte um controlador com useOverlayUrlState, de @reelkit/vue, e entregue-o ao ReelPlayerUrlOverlay como controller: quem manda no player é a barra de endereços, então ele abre quando o parâmetro nomeia um slide e fecha quando o parâmetro some. Abrir empilha uma entrada no histórico e cada troca de slide a substitui, então percorrer um feed não acrescenta entradas e um único passo para trás sempre sai. A profundidade da URL acompanha a chave do controlador: um urlIndexKey de eixo único endereça apenas o post (?reel=3); um urlIndexTwoAxisKey de dois eixos carrega também o índice da mídia interna de um post com várias mídias (?reel=3.2); escolha uma chave por app, porque os dois formatos não são decodificados um pelo outro. É um componente separado do ReelPlayerOverlay, então cada um carrega exatamente um comando do estado aberto — o modelo is-open ou o controller da URL, nunca os dois.

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/vue. Veja o guia de estado na URL e a API do core.

Um app com rotas deve passar um adaptador ligado ao roteador, para que ele siga sendo a única fonte de verdade da navegação — escrever no histórico por trás dele deixa sua localização desatualizada e derruba o parâmetro na navegação seguinte. useVueRouterUrlAdapter, de @reelkit/vue/vue-router-url-adapter, é o adaptador pronto para o Vue Router.

vue
<script setup lang="ts">
import { ReelPlayerUrlOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/vue';
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';
import '@reelkit/vue-reel-player/styles.css';

const props = defineProps<{ content: ContentItem[] }>();

const reel = useOverlayUrlState({
  param: 'reel',
  adapter: useVueRouterUrlAdapter(),
  ...urlIndexKey(() => props.content.length),
});
</script>

<template>
  <!-- Opening is a link — the overlay reads the URL and opens itself. -->
  <RouterLink v-for="(post, i) in props.content" :key="post.id" :to="`?reel=${i}`">
    <img :src="post.media[0].src" />
  </RouterLink>

  <ReelPlayerUrlOverlay :controller="reel" :content="props.content" />
</template>

As opções completas de useOverlayUrlState estão na referência da API para Vue.

  • 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.
  • A profundidade da URL acompanha a chave do controlador: eixo único para o post apenas, ou dois eixos (urlIndexTwoAxisKey) para carregar também o índice da imagem interna de um post com várias mídias. Escolha uma chave por app; os formatos não são decodificados um pelo outro.

Uma chave ou duas — escolha a profundidade da URL

O mesmo ReelPlayerOverlay 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.

typescript
import { useOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/vue';

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

// A link now names both axes: post 3, inner media 2 — ?reel=3.2

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.

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

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

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

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

ReelPlayerOverlayProps<T>

PropTipoPadrãoDescrição
ariaLabelstring'Video player'Rótulo acessível da região do diálogo; anunciado pelos leitores de tela quando o overlay abre
aspectRationumber9 / 16Proporção entre largura e altura do contêiner no desktop. Com 768px de largura ou menos o player ocupa a tela inteira. Um valor zero, negativo ou não finito volta para 9/16.
contentT[] (extends BaseContentItem)obrigatórioArray de itens de conteúdo a exibir no player
enableNavKeysbooleantrueLiga a navegação pelas setas do teclado
enableWheelbooleantrueLiga a navegação pela roda do mouse
initialIndexnumber0Índice, começando em zero, do item visível no início
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 leitor navega.
isOpenbooleanobrigatórioControla a visibilidade do overlay; com false, o overlay sai do DOM
loopbooleanfalseLiga o ciclo infinito entre os slides
swipeDistanceFactornumber0.12Fração mínima da distância de deslize para trocar de slide
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 o slot #timeline 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.
transitionDurationnumber300Duração da animação do slide em ms
wheelDebounceMsnumber200Duração do debounce dos eventos de roda em ms

Props do ReelPlayerUrlOverlay

ReelPlayerUrlOverlayProps<T>

Aceita todas as props acima, menos is-open, substituída por controller. initial-index é 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 position 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.

Eventos

EventoCargaDescrição
@api-readyReelPlayerApiEmitido assim que o slider fica pronto, expondo a API imperativa
@closevoidEmitido quando o player fecha
@slide-changenumberEmitido com o novo índice do slide ativo depois de uma troca
@inner-slide-changeouter: number, inner: numberEmitido quando o índice da mídia interna do post ativo muda — na navegação interna e na ativação externa (o índice interno atual do post ativado, 0 para mídia única).
@update:is-openbooleanEmitido no fechamento; é o que viabiliza o `v-model:is-open`

v-model:is-open

Use v-model:is-open para conduzir o overlay com uma única ligação. O padrão antigo de :is-open com @close continua funcionando, se você precisar do evento explícito.

vue
<template>
  <button @click="open = true">Open</button>
  <ReelPlayerOverlay v-model:is-open="open" :content="content" />
</template>

Tipos

ContentItem

CampoTipoDescrição
idstringIdentificador único
mediaMediaItem[]Uma ou mais mídias (imagem ou vídeo)
author{ name: string; avatar?: string }Autor mostrado na camada padrão do slide
descriptionstring?Texto da legenda
likesnumber?Contagem de curtidas

TimelineBarProps

typescript
interface TimelineBarProps {
  class?: string;
  style?: CSSProperties;
}

TimelineSlotScope<T>

typescript
interface TimelineSlotScope<T extends BaseContentItem> {
  item: T;
  activeIndex: number;
  timelineState: TimelineController;
  defaultContent: () => VNode | VNode[];
}

MediaItem

CampoTipoDescrição
idstringIdentificador único
type'image' | 'video'Tipo da mídia
srcstringURL do arquivo de mídia
posterstring?URL da miniatura de pôster, para itens de vídeo
aspectRationumberProporção entre largura e altura. Valores < 1 = vertical (cover), ≥ 1 = horizontal (contain).

Uniões

TipoValoresDescrição
MediaType'image' | 'video'Tipo de um MediaItem; decide se o slide renderiza ImageSlide ou VideoSlide.
TimelineMode'auto' | 'always' | 'never'Valor da prop timeline: 'auto' mostra a barra para vídeos mais longos que timelineMinDurationSeconds, 'always' para todo vídeo, 'never' a esconde.

Subcomponentes

Use-os dentro dos seus templates de #controls, #slide ou #slideOverlay. Repasse as dimensões e os callbacks do escopo do slot para que a reprodução automática, a captura de pôster e a sincronia do som continuem funcionando.

CloseButton

Botão circular de fechar isolado, já com o estilo do reel player. Use dentro de #controls.

vue
<script setup lang="ts">
import { CloseButton } from '@reelkit/vue-reel-player';
</script>

<template>
  <CloseButton :on-click="onClose" />
  <CloseButton :on-click="onClose" class-name="my-close-btn" :style="{ top: '24px', right: '24px' }" />
</template>

SoundButton

Botão de mudo. Renderize-o dentro de um SoundProvider (o ReelPlayerOverlay fornece um). Fica escondido quando o slide ativo não tem vídeo.

vue
<script setup lang="ts">
import { SoundButton } from '@reelkit/vue-reel-player';
</script>

<template>
  <SoundButton />
  <SoundButton disabled class-name="my-sound-btn" />
</template>

PlayerControls

Controles padrão que o overlay renderiza quando nenhum slot #controls é passado: um CloseButton e, com show-sound, um SoundButton. Props: showSound, soundDisabled (esmaece o botão de som) e onClose. Coloque-o dentro do seu próprio slot #controls para manter o visual embutido ao lado de botões extras.

LoadingIndicator e ErrorIndicator

LoadingIndicator é o indicador de onda em tela cheia mostrado enquanto a mídia carrega; ErrorIndicator é a mensagem padrão mostrada quando a mídia falha. Nenhum dos dois recebe props. Renderize-os a partir do seu próprio slot #loading ou #error para manter o visual embutido em volta de conteúdo extra.

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 pelo slot #timeline.

vue
<script setup lang="ts">
import type { TimelineSlotScope } from '@reelkit/vue-reel-player';
</script>

<template>
  <!-- Wrap or augment the default bar from #timeline: -->
  <ReelPlayerOverlay>
    <template #timeline="{ defaultContent }: TimelineSlotScope">
      <MyTimecode />
      <component :is="defaultContent" />
    </template>
  </ReelPlayerOverlay>
</template>

SlideOverlay

A camada em gradiente padrão, que mostra autor, descrição e curtidas. Aparece quando o conteúdo traz esses campos. Substitua-a ou esconda-a pelo slot #slideOverlay.

vue
<script setup lang="ts">
import { SlideOverlay } from '@reelkit/vue-reel-player';
</script>

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

ImageSlide

Slide de imagem com carregamento tardio e object-fit: cover por padrão. Componha-o dentro do slot #slide para personalizar a exibição da imagem mantendo o comportamento embutido.

vue
<script setup lang="ts">
import { ImageSlide } from '@reelkit/vue-reel-player';
</script>

<template>
  <ImageSlide :src="media.src" :size="size" />

  <ImageSlide
    :src="media.src"
    :size="size"
    class-name="my-image-slide"
    :style="{ backgroundColor: '#1a1a1a', borderRadius: '12px' }"
    :img-style="{ objectFit: 'contain' }"
  />
</template>

VideoSlide

Slide de vídeo apoiado em um elemento <video> compartilhado. Cuida da continuidade do som no iOS, dos quadros de pôster e da memória de posição. Renderize-o dentro de um SoundProvider (o ReelPlayerOverlay fornece um).

vue
<script setup lang="ts">
import { VideoSlide } from '@reelkit/vue-reel-player';
</script>

<template>
  <VideoSlide
    :src="media.src"
    :poster="media.poster"
    :aspect-ratio="9 / 16"
    :size="size"
    :is-active="isActive"
    :slide-key="slideKey"
    :style="{ borderRadius: '12px' }"
  />
</template>
Compondo slides próprios

Use #slide 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).

vue
<script setup lang="ts">
import {
  ReelPlayerOverlay,
  ImageSlide,
  VideoSlide,
} from '@reelkit/vue-reel-player';
</script>

<template>
  <ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
    <template #slide="{ item, size, isActive, slideKey }">
      <ImageSlide
        v-if="item.media[0].type === 'image'"
        :src="item.media[0].src"
        :size="size"
        :style="{ backgroundColor: '#111' }"
        :img-style="{ objectFit: 'contain' }"
      />
      <VideoSlide
        v-else
        :src="item.media[0].src"
        :poster="item.media[0].poster"
        :aspect-ratio="item.media[0].aspectRatio"
        :size="size"
        :is-active="isActive"
        :slide-key="slideKey"
        :style="{ borderRadius: '16px' }"
      />
    </template>
  </ReelPlayerOverlay>
</template>

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; mídias quebradas mostram um ícone de erro. O player guarda em cache as URLs que falharam, então reabrir um slide quebrado pula a nova tentativa.

Callbacks de ciclo de vida

Ao usar o slot #slide, chame estes callbacks, vindos do escopo do slot, 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.
vue
<!-- Inside #slide — wire callbacks to your custom media -->
<template #slide="{ item, size, isActive, onReady, onWaiting, onError }">
  <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
    <img
      v-if="item.media[0].type === 'image'"
      :src="item.media[0].src"
      @load="onReady"
      @error="onError"
      style="width:100%;height:100%;object-fit:cover"
    />
    <video
      v-else
      :src="item.media[0].src"
      :autoplay="isActive"
      @canplay="onReady"
      @waiting="onWaiting"
      @error="onError"
      style="width:100%;height:100%;object-fit:cover"
    />
  </div>
</template>

Interface própria de carregamento e erro

Troque a animação de onda e o ícone de erro padrão pelos slots #loading e #error:

vue
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
  <template #loading="{ activeIndex }">
    <div
      style="position:absolute;inset:0;z-index:10;display:flex;
             align-items:center;justify-content:center;color:#fff;font-size:14px"
    >
      Loading slide {{ activeIndex + 1 }}...
    </div>
  </template>

  <template #error="{ activeIndex }">
    <div
      style="position:absolute;inset:0;z-index:10;display:flex;
             flex-direction:column;align-items:center;justify-content:center;
             gap:12px;color:rgba(255,255,255,0.5)"
    >
      <span style="font-size:48px">!</span>
      <span>Slide {{ activeIndex + 1 }} failed to load</span>
    </div>
  </template>
</ReelPlayerOverlay>

Linha do tempo

O overlay desenha uma barra de reprodução embutida sobre o vídeo ativo. Libere-a pela prop timeline: 'auto' (padrão) aparece sempre que a mídia ativa for um vídeo mais longo que timelineMinDurationSeconds (30 por padrão), 'always' sempre que houver um vídeo ativo, 'never' para desligar. Para uma barra totalmente sua, use o slot #timeline; o escopo dele expõe um timelineState apoiado no TimelineController por baixo.

vue
<ReelPlayerOverlay
  :is-open="isOpen"
  :content="items"
  timeline="auto"
  :timeline-min-duration-seconds="30"
  @close="isOpen = false"
/>

Personalize pelas propriedades CSS --rk-reel-timeline-*.

Fora do slot, leia o mesmo controlador com um composable:

ExportDescrição
useTimelineState()Devolve o TimelineController mais próximo; lança um erro fora de um TimelineProvider (o overlay monta um).
useTimelineStateOptional()Igual a useTimelineState, mas devolve null fora de um TimelineProvider em vez de lançar um erro — para componentes que podem renderizar fora do overlay.
RK_TIMELINE_KEYChave de injeção que os dois composables leem; faça provide dela você mesmo para controlar um TimelineBar com o seu próprio controlador.
useViewportSize(aspectRatio)[width, height] reativo com que o overlay se dimensiona: a proporção dada no desktop, a viewport inteira com 768px de largura ou menos. Devolve [0, 0] até montar, então é seguro durante o SSR.

Contexto de som

O ReelPlayerOverlay monta um SoundProvider na raiz, então qualquer componente renderizado lá dentro pode ler ou alternar o estado de mudo por useSoundState. O composable é reexportado por @reelkit/vue-reel-player, então você não precisa de um import separado de @reelkit/vue.

vue
<script setup lang="ts">
import { useSoundState, toVueRef } from '@reelkit/vue';

// Inside a custom control rendered from the #controls slot:
const soundState = useSoundState();
const muted = toVueRef(soundState.muted);
</script>

<template>
  <button @click="soundState.toggle()">
    {{ muted ? 'Unmute' : 'Mute' }}
  </button>
</template>

Dentro do player, o slot #controls também expõe soundState no seu escopo. Prefira essa via quando precisar do estado apenas no template dos controles.

Classes CSS

As classes CSS são simples (sem escopo). Uma folha de estilo carregada depois de @reelkit/vue-reel-player/styles.css pode sobrescrever qualquer uma delas com um seletor de especificidade maior. Para mudar cor, tamanho e z-index, use as propriedades CSS da seção Temas, abaixo.

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 do seu slot `#timeline` 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. Os tokens são os mesmos de @reelkit/react-reel-player, então as sobrescritas valem para os dois bindings.

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-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-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-size36pxTamanho das setas aninhadas
--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-transition0.15s ease-outAnimação de crescer e encolher da trilha e da pílula

Cole o trecho abaixo em uma folha de estilo carregada depois de @reelkit/vue-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 a prop aria-label para mudar o que o leitor de tela anuncia; o padrão é "Video player". Cada slide carrega role="group", aria-roledescription="slide" e aria-label="Slide N of M".

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 (carrossel aninhado)
ArrowRightPróxima mídia (carrossel aninhado)
EscapeFecha o player