HextaUI

Video player

Um player de vídeo com barra de busca arrastável, controles que se ocultam sozinhos, atalhos de teclado, velocidade, picture in picture e tela cheia.

0:00 / --:--
import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPictureInPictureButton,
  VideoPlayerPlaybackRate,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSeekButton,
  VideoPlayerSpacer,
  VideoPlayerTime,
  VideoPlayerVolume,
} from "@/components/ui/video-player"

export function VideoPlayerDemo() {
  return (
    <VideoPlayer className="max-w-2xl">
      <VideoPlayerContent
        poster="https://media.w3.org/2010/05/sintel/poster.png"
        aria-label="Sintel trailer"
      >
        <source
          src="https://media.w3.org/2010/05/sintel/trailer.webm"
          type="video/webm"
        />
        <source
          src="https://media.w3.org/2010/05/sintel/trailer.mp4"
          type="video/mp4"
        />
      </VideoPlayerContent>
      <VideoPlayerControls>
        <VideoPlayerSeekBar />
        <VideoPlayerPlayButton />
        <VideoPlayerSeekButton offset={-10} />
        <VideoPlayerSeekButton offset={10} />
        <VideoPlayerVolume />
        <VideoPlayerTime />
        <VideoPlayerSpacer />
        <VideoPlayerPlaybackRate />
        <VideoPlayerPictureInPictureButton />
        <VideoPlayerFullscreenButton />
      </VideoPlayerControls>
    </VideoPlayer>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/video-player.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSpacer,
  VideoPlayerTime,
  VideoPlayerVolume,
} from "@/components/ui/video-player"
<VideoPlayer>
  <VideoPlayerContent src="/intro.mp4" poster="/intro.jpg" />
  <VideoPlayerControls>
    <VideoPlayerSeekBar />
    <VideoPlayerPlayButton />
    <VideoPlayerVolume />
    <VideoPlayerTime />
    <VideoPlayerSpacer />
    <VideoPlayerFullscreenButton />
  </VideoPlayerControls>
</VideoPlayer>
VideoPlayer
├── VideoPlayerContent
└── VideoPlayerControls
    ├── VideoPlayerPlayButton
    ├── VideoPlayerSeekButton
    ├── VideoPlayerSeekBar
    ├── VideoPlayerTime
    ├── VideoPlayerSpacer
    ├── VideoPlayerVolume
    ├── VideoPlayerPlaybackRate
    ├── VideoPlayerCaptionsButton
    ├── VideoPlayerPictureInPictureButton
    └── VideoPlayerFullscreenButton

Barra

variant="bar" coloca os controles sob a imagem, na superfície da página. Eles nunca ocultam nem cobrem o vídeo.

import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPictureInPictureButton,
  VideoPlayerPlaybackRate,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSeekButton,
  VideoPlayerSpacer,
  VideoPlayerTime,
  VideoPlayerVolume,
} from "@/components/ui/video-player"

export function VideoPlayerBar() {
  return (
    <VideoPlayer variant="bar" className="max-w-2xl">
      <VideoPlayerContent
        poster="https://media.w3.org/2010/05/sintel/poster.png"
        aria-label="Sintel trailer"
      >
        <source
          src="https://media.w3.org/2010/05/sintel/trailer.webm"
          type="video/webm"
        />
        <source
          src="https://media.w3.org/2010/05/sintel/trailer.mp4"
          type="video/mp4"
        />
      </VideoPlayerContent>
      <VideoPlayerControls>
        <VideoPlayerSeekBar />
        <VideoPlayerPlayButton />
        <VideoPlayerSeekButton offset={-10} />
        <VideoPlayerSeekButton offset={10} />
        <VideoPlayerVolume />
        <VideoPlayerTime />
        <VideoPlayerSpacer />
        <VideoPlayerPlaybackRate />
        <VideoPlayerPictureInPictureButton />
        <VideoPlayerFullscreenButton />
      </VideoPlayerControls>
    </VideoPlayer>
  )
}

Mínimo

Use apenas as partes de que precisa. tooltips={false} desativa as dicas de hover, e type="remaining" conta regressivamente em vez de progressivamente.

import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSpacer,
  VideoPlayerTime,
} from "@/components/ui/video-player"

export function VideoPlayerMinimal() {
  return (
    <VideoPlayer className="max-w-md">
      <VideoPlayerContent
        src="https://media.w3.org/2010/05/sintel/trailer.mp4"
        poster="https://media.w3.org/2010/05/sintel/poster.png"
        aria-label="Sintel trailer"
      />
      <VideoPlayerControls tooltips={false}>
        <VideoPlayerSeekBar />
        <VideoPlayerPlayButton />
        <VideoPlayerTime type="remaining" />
        <VideoPlayerSpacer />
        <VideoPlayerFullscreenButton />
      </VideoPlayerControls>
    </VideoPlayer>
  )
}

Deslocamentos de busca e velocidades

offset define quanto cada botão de busca salta, e rates define as velocidades do menu.

import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerPlaybackRate,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSeekButton,
  VideoPlayerSpacer,
  VideoPlayerTime,
} from "@/components/ui/video-player"

export function VideoPlayerSeekOffsets() {
  return (
    <VideoPlayer className="max-w-xl">
      <VideoPlayerContent
        src="https://media.w3.org/2010/05/sintel/trailer.mp4"
        poster="https://media.w3.org/2010/05/sintel/poster.png"
        aria-label="Sintel trailer"
      />
      <VideoPlayerControls>
        <VideoPlayerSeekBar />
        <VideoPlayerSeekButton offset={-15} />
        <VideoPlayerPlayButton />
        <VideoPlayerSeekButton offset={30} />
        <VideoPlayerTime />
        <VideoPlayerSpacer />
        <VideoPlayerPlaybackRate rates={[1, 1.5, 2, 3]} />
      </VideoPlayerControls>
    </VideoPlayer>
  )
}

Atalhos na página inteira

Os atalhos funcionam enquanto o foco está dentro do player. globalShortcuts também escuta na página, mas nunca enquanto você digita em um campo, usa um botão ou tem um menu ou dialog aberto. Use para um player por página.

import { Kbd } from "@/components/ui/kbd"
import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSpacer,
  VideoPlayerTime,
  VideoPlayerVolume,
} from "@/components/ui/video-player"

export function VideoPlayerGlobalShortcuts() {
  return (
    <div className="flex w-full max-w-xl flex-col items-center gap-3">
      <VideoPlayer globalShortcuts>
        <VideoPlayerContent
          src="https://media.w3.org/2010/05/sintel/trailer.mp4"
          poster="https://media.w3.org/2010/05/sintel/poster.png"
          aria-label="Sintel trailer"
        />
        <VideoPlayerControls>
          <VideoPlayerSeekBar />
          <VideoPlayerPlayButton />
          <VideoPlayerVolume />
          <VideoPlayerTime />
          <VideoPlayerSpacer />
          <VideoPlayerFullscreenButton />
        </VideoPlayerControls>
      </VideoPlayer>
      <p className="text-sm text-muted-foreground">
        Press <Kbd keys="k" /> anywhere on the page to play or pause.
      </p>
    </div>
  )
}

Legendas

Adicione um <track> e VideoPlayerCaptionsButton. As legendas são desenhadas pelo player, então sobem enquanto os controles aparecem em vez de ficarem escondidas atrás deles. Uma faixa de outra origem precisa de crossOrigin no vídeo.

import {
  VideoPlayer,
  VideoPlayerCaptionsButton,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSpacer,
  VideoPlayerTime,
  VideoPlayerVolume,
} from "@/components/ui/video-player"

export function VideoPlayerCaptions() {
  return (
    <VideoPlayer className="max-w-xl">
      <VideoPlayerContent
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/friday.mp4"
        crossOrigin="anonymous"
        aria-label="Friday"
      >
        <track
          default
          kind="captions"
          srcLang="en"
          label="English"
          src="https://interactive-examples.mdn.mozilla.net/media/examples/friday.vtt"
        />
      </VideoPlayerContent>
      <VideoPlayerControls>
        <VideoPlayerSeekBar />
        <VideoPlayerPlayButton />
        <VideoPlayerVolume />
        <VideoPlayerTime />
        <VideoPlayerSpacer />
        <VideoPlayerCaptionsButton />
        <VideoPlayerFullscreenButton />
      </VideoPlayerControls>
    </VideoPlayer>
  )
}

Controles personalizados

useVideoPlayer lê estado e ações de qualquer componente dentro do player. Selecione apenas o que usa, para o componente renderizar de novo somente quando esse valor mudar.

"use client"

import { Button } from "@/components/ui/button"
import {
  formatTime,
  useVideoPlayer,
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerTime,
} from "@/components/ui/video-player"

const chapters = [
  { title: "The cave", time: 0 },
  { title: "Searching", time: 13 },
  { title: "The dragon", time: 31 },
]

function Chapters() {
  const seek = useVideoPlayer((player) => player.seek)
  const play = useVideoPlayer((player) => player.play)
  const currentTime = useVideoPlayer((player) => Math.floor(player.currentTime))
  const active = chapters.findLast((chapter) => chapter.time <= currentTime)

  return (
    <div className="flex flex-wrap gap-2">
      {chapters.map((chapter) => (
        <Button
          key={chapter.title}
          variant={chapter === active ? "secondary" : "outline"}
          size="sm"
          aria-pressed={chapter === active}
          onClick={() => {
            seek(chapter.time)
            play()
          }}
        >
          <span className="tabular-nums">{formatTime(chapter.time)}</span>
          {chapter.title}
        </Button>
      ))}
    </div>
  )
}

export function VideoPlayerCustomControls() {
  return (
    <VideoPlayer variant="bar" className="max-w-xl">
      <VideoPlayerContent
        src="https://media.w3.org/2010/05/sintel/trailer.mp4"
        poster="https://media.w3.org/2010/05/sintel/poster.png"
        aria-label="Sintel trailer"
      />
      <VideoPlayerControls>
        <VideoPlayerSeekBar />
        <VideoPlayerPlayButton />
        <VideoPlayerTime />
        <div className="basis-full px-1 pt-1">
          <Chapters />
        </div>
      </VideoPlayerControls>
    </VideoPlayer>
  )
}

Erro

Quando a fonte falha, o player mostra errorMessage, anuncia e desabilita os controles que não podem funcionar.

import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerTime,
} from "@/components/ui/video-player"

export function VideoPlayerError() {
  return (
    <VideoPlayer
      className="max-w-md"
      errorMessage="We couldn’t load this video. Check your connection and try again."
    >
      <VideoPlayerContent
        src="https://media.w3.org/2010/05/sintel/missing.mp4"
        aria-label="Missing video"
      />
      <VideoPlayerControls>
        <VideoPlayerSeekBar />
        <VideoPlayerPlayButton />
        <VideoPlayerTime />
      </VideoPlayerControls>
    </VideoPlayer>
  )
}

Da direita para a esquerda

Os rótulos seguem o idioma da página. A linha do tempo e os controles de reprodução permanecem da esquerda para a direita, como nos players de mídia das plataformas.

import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControls,
  VideoPlayerFullscreenButton,
  VideoPlayerPlaybackRate,
  VideoPlayerPlayButton,
  VideoPlayerSeekBar,
  VideoPlayerSpacer,
  VideoPlayerTime,
  VideoPlayerVolume,
} from "@/components/ui/video-player"

export function VideoPlayerRtl() {
  return (
    <div dir="rtl" className="w-full max-w-xl">
      <VideoPlayer aria-label="مشغل الفيديو">
        <VideoPlayerContent
          src="https://media.w3.org/2010/05/sintel/trailer.mp4"
          poster="https://media.w3.org/2010/05/sintel/poster.png"
          aria-label="إعلان سينتل"
        />
        <VideoPlayerControls>
          <VideoPlayerSeekBar label="تقديم" />
          <VideoPlayerPlayButton
            playLabel="تشغيل"
            pauseLabel="إيقاف مؤقت"
            replayLabel="إعادة التشغيل"
          />
          <VideoPlayerVolume
            label="مستوى الصوت"
            muteLabel="كتم الصوت"
            unmuteLabel="إلغاء كتم الصوت"
          />
          <VideoPlayerTime />
          <VideoPlayerSpacer />
          <VideoPlayerPlaybackRate label="سرعة التشغيل" normalLabel="عادي" />
          <VideoPlayerFullscreenButton
            enterLabel="ملء الشاشة"
            exitLabel="الخروج من ملء الشاشة"
          />
        </VideoPlayerControls>
      </VideoPlayer>
    </div>
  )
}

Funcionam enquanto o foco está em qualquer lugar dentro do player, ou na página com globalShortcuts. São ignorados enquanto uma tecla modificadora está segurada ou o foco está em um campo de texto.

TeclaAção
SpaceKReproduz ou pausa.
JVolta 10 segundos.
LAvança 10 segundos.
←→Volta ou avança 5 segundos. Na barra de busca, Shift salta 10.
↑↓Aumenta ou diminui o volume em 5%.
MSilencia ou reativa o som.
CLiga ou desliga as legendas, quando o vídeo as tem.
FEntra ou sai da tela cheia.
IAbre ou fecha o picture in picture, onde houver suporte.
Shift+.Shift+,Acelera ou desacelera a reprodução.
0–9Salta de 0% a 90% do vídeo.
HomeEndSalta para o início ou o fim.
  • O player é uma região rotulada. Todo botão tem um nome que acompanha seu estado (Play, Pause, Replay) e um tooltip com seu atalho.
  • A barra de busca e o volume são sliders. A barra de busca lê seu valor como “1 minute 5 seconds of 3 minutes”.
  • Ações de atalhos e cliques no vídeo são anunciadas de forma educada, por exemplo “Paused” ou “Volume 40%”. Erros de carregamento são anunciados como um alerta.
  • Na variante overlay, os controles somem com fade após 2,5 segundos de reprodução sem movimento do ponteiro. Permanecem visíveis enquanto pausado, enquanto você passa o mouse ou os usa com o teclado, e enquanto um menu está aberto.
  • Em telas touch, um toque mostra ou oculta os controles e um toque duplo no terço esquerdo ou direito volta ou avança 10 segundos. Continue tocando para somar 10 segundos a cada vez.
  • O botão de legendas é um toggle com aria-pressed. Ele escolhe a última faixa que você usou, depois uma no idioma do navegador, depois a primeira.
  • Com movimento reduzido, controles e feedback aparecem com fade, sem mover nem escalar.

A barra de busca e o volume são construídos sobre o slider do Base UI, e os botões sobre Button, Tooltip e Dropdown menu do HextaUI.

PropTipoPadrão
variantoverlay faz controles de ocultação automática flutuarem sobre o vídeo. bar os coloca abaixo dele.
"overlay" | "bar""overlay"
shortcutsAtalhos de teclado enquanto o foco está no player.
booleantrue
globalShortcutsEscute também os atalhos na página inteira.
booleanfalse
errorMessage
ReactNode"This video can’t be played."
AtributoDescrição
data-slot="video-player"Selecione a raiz no CSS.
data-variantA variante atual.
data-controls"visible" ou "hidden". O cursor se oculta junto com os controles.
data-fullscreenPresente enquanto o player está em tela cheia.
aria-busyDefinido enquanto a reprodução espera por dados.

O elemento <video>. Aceita todos os atributos de vídeo e filhos <source> ou <track>. Um clique reproduz ou pausa, um clique duplo alterna a tela cheia, um toque mostra ou oculta os controles e um toque duplo em qualquer lado faz a busca.

PropTipoPadrão
autoPlayInicia a reprodução na montagem, exceto com movimento reduzido.
booleanfalse
playsInline
booleantrue
preload
"none" | "metadata" | "auto""metadata"
doubleTapSeekSegundos que um toque duplo em qualquer lado salta em telas touch. false desativa.
number | false10
renderTroque por outro elemento de mídia, como um elemento de vídeo HLS.
ReactElement | (props, state) => ReactElement<video>
AtributoDescrição
data-slot="video-player-content"Seleciona o vídeo no CSS.
PropTipoPadrão
tooltipsMostra o rótulo e o atalho de cada controle no hover.
booleantrue
AtributoDescrição
data-slot="video-player-controls"Seleciona a barra de controles no CSS.
data-hiddenPresente enquanto os controles do overlay estão ocultos.

Sempre ocupa sua própria linha acima dos botões. O hover mostra o tempo sob o ponteiro, e a trilha mais clara mostra o que já foi carregado.

PropTipoPadrão
label
string"Seek"
onValueChange
(value: number, details) => void–
onValueCommitted
(value: number, details) => void–
disabled
booleanfalse
AtributoDescrição
data-slot="video-player-seek-bar"Seleciona a barra de busca no CSS.
data-draggingPresente enquanto você arrasta a barra.
data-previewingPresente no controle enquanto o tempo do hover é exibido.
--video-player-bufferedA parte carregada do vídeo, de 0 a 1.
--video-player-hoverA posição do ponteiro ao longo da barra, de 0 a 1.
PropTipoPadrão
playLabel
string"Play"
pauseLabel
string"Pause"
replayLabel
string"Replay"
...propsTodas as props do Button, incluindo variant e size.
ButtonProps–
AtributoDescrição
data-slot="video-player-play-button"Seleciona o botão no CSS.
data-state"paused", "playing" ou "ended".
PropTipoPadrão
offsetSegundos a saltar. Valores negativos voltam.
number10
label
string"Forward 10 seconds"
...propsTodas as props do Button, incluindo variant e size.
ButtonProps–
AtributoDescrição
data-slot="video-player-seek-button"Seleciona o botão no CSS.
data-direction"backward" ou "forward".

Um botão de mudo com um slider que abre no hover ou no foco. Em telas touch só aparece o botão de mudo, já que os celulares controlam o volume com os próprios botões.

PropTipoPadrão
label
string"Volume"
muteLabel
string"Mute"
unmuteLabel
string"Unmute"
AtributoDescrição
data-slot="video-player-volume"Selecione o grupo no CSS.
data-slot="video-player-mute-button"O botão de mudo. Também exportado como VideoPlayerMuteButton.
data-stateNo botão de mudo: "muted", "low" ou "high".
PropTipoPadrão
type
"both" | "elapsed" | "remaining" | "duration""both"
AtributoDescrição
data-slot="video-player-time"Seleciona o tempo no CSS.
data-typeO tipo atual.
PropTipoPadrão
rates
number[][0.5, 0.75, 1, 1.25, 1.5, 2]
label
string"Playback speed"
normalLabel
string"Normal"
AtributoDescrição
data-slot="video-player-playback-rate"Seleciona o gatilho do menu no CSS.

Coloca o player inteiro em tela cheia, ou o próprio vídeo no iPhone. Não renderiza nada onde a tela cheia não está disponível.

PropTipoPadrão
enterLabel
string"Full screen"
exitLabel
string"Exit full screen"
AtributoDescrição
data-slot="video-player-fullscreen-button"Seleciona o botão no CSS.
data-state"on" ou "off".

Não renderiza nada até o vídeo ter uma faixa de legendas.

PropTipoPadrão
label
string"Captions"
AtributoDescrição
data-slot="video-player-captions-button"Seleciona o botão no CSS.
data-state"on" ou "off".
data-slot="video-player-captions"O texto da legenda sobre o vídeo. data-lifted está presente enquanto ela fica acima dos controles.

Não renderiza nada em navegadores sem picture in picture.

PropTipoPadrão
enterLabel
string"Picture in picture"
exitLabel
string"Exit picture in picture"
AtributoDescrição
data-slot="video-player-pip-button"Seleciona o botão no CSS.
data-state"on" ou "off".

Preenche o espaço livre na linha de controles, empurrando os controles seguintes para o fim.

Retorna o estado e as ações do player. Passe um seletor que retorne um único valor.

const paused = useVideoPlayer((player) => player.paused)
const seek = useVideoPlayer((player) => player.seek)
PropTipoPadrão
state
paused, ended, started, waiting, scrubbing, currentTime, duration, buffered, volume, muted, playbackRate, fullscreen, pictureInPicture, error, hasCaptions, captions, caption–
actions
play, pause, togglePaused, seek, seekBy, setVolume, toggleMuted, setPlaybackRate, toggleFullscreen, togglePictureInPicture, toggleCaptions–