HextaUI

Video player

Un reproductor de vídeo con barra de búsqueda arrastrable, controles que se ocultan solos, atajos de teclado, velocidad, imagen en imagen y pantalla completa.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que 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 los controles bajo la imagen, sobre la superficie de la página. Nunca ocultan ni cubren el 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

Usa solo las partes que necesites. tooltips={false} desactiva las pistas al pasar el cursor, y type="remaining" cuenta hacia atrás en lugar de hacia delante.

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

Saltos de búsqueda y velocidades

offset define cuánto salta cada botón de búsqueda, y rates define las velocidades del menú.

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

Atajos en toda la página

Los atajos funcionan mientras el foco está dentro del reproductor. globalShortcuts también escucha en la página, pero nunca mientras escribes en un campo, usas un botón o tienes un menú o dialog abierto. Úsalo para un solo reproductor 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>
  )
}

Subtítulos

Añade un <track> y VideoPlayerCaptionsButton. Los subtítulos los dibuja el reproductor, así que suben mientras se muestran los controles en lugar de quedar ocultos tras ellos. Una pista de otro origen necesita crossOrigin en el 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 lee el estado y las acciones desde cualquier componente dentro del reproductor. Selecciona solo lo que uses, para que el componente se vuelva a renderizar únicamente cuando ese valor cambie.

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

Error

Cuando la fuente falla, el reproductor muestra errorMessage, lo anuncia y deshabilita los controles que no pueden 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>
  )
}

De derecha a izquierda

Las etiquetas siguen el idioma de la página. La línea de tiempo y los controles de transporte permanecen de izquierda a derecha, como hacen los reproductores multimedia de las 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>
  )
}

Funcionan mientras el foco está en cualquier lugar dentro del reproductor, o en la página con globalShortcuts. Se omiten mientras se mantiene una tecla modificadora o el foco está en un campo de texto.

KeyAcción
SpaceKReproduce o pausa.
JRetrocede 10 segundos.
LAvanza 10 segundos.
←→Retrocede o avanza 5 segundos. En la barra de búsqueda, Shift salta 10.
↑↓Sube o baja el volumen un 5%.
MSilencia o activa el sonido.
CActiva o desactiva los subtítulos, cuando el vídeo los tiene.
FEntra o sale de la pantalla completa.
IAbre o cierra la imagen en imagen, donde sea compatible.
Shift+.Shift+,Acelera o ralentiza la reproducción.
0–9Salta del 0% al 90% del vídeo.
HomeEndSalta al inicio o al final.
  • El reproductor es una región con etiqueta. Cada botón tiene un nombre que sigue su estado (Play, Pause, Replay) y un tooltip con su atajo.
  • La barra de búsqueda y el volumen son sliders. La barra de búsqueda lee su valor como “1 minuto 5 segundos de 3 minutos”.
  • Las acciones de los atajos y de los clics en el vídeo se anuncian de forma educada, por ejemplo “Paused” o “Volume 40%”. Los errores de carga se anuncian como una alerta.
  • En la variante overlay, los controles se desvanecen tras 2.5 segundos de reproducción sin movimiento del puntero. Permanecen visibles en pausa, mientras pasas el cursor o los usas con el teclado, y mientras hay un menú abierto.
  • En pantallas táctiles, un toque muestra u oculta los controles y un doble toque en el tercio izquierdo o derecho retrocede o avanza 10 segundos. Sigue tocando para añadir 10 segundos cada vez.
  • El botón de subtítulos es un toggle con aria-pressed. Elige la última pista que usaste, luego una en el idioma del navegador y después la primera.
  • Con movimiento reducido, los controles y el feedback se desvanecen sin moverse ni escalar.

La barra de búsqueda y el volumen están construidos sobre el slider de Base UI, y los botones sobre Button, Tooltip y Dropdown menu de HextaUI.

PropTipoPredeterminado
variantoverlay coloca controles que se ocultan solos sobre el vídeo. bar los coloca debajo.
"overlay" | "bar""overlay"
shortcutsAtajos de teclado mientras el foco está en el reproductor.
booleantrue
globalShortcutsEscucha también los atajos en toda la página.
booleanfalse
errorMessage
ReactNode"This video can’t be played."
AtributoDescripción
data-slot="video-player"Apunta a la raíz en CSS.
data-variantLa variante actual.
data-controls"visible" o "hidden". El cursor se oculta junto con los controles.
data-fullscreenPresente mientras el reproductor está a pantalla completa.
aria-busySe define mientras la reproducción espera datos.

El elemento <video>. Acepta todos los atributos de video, y hijos <source> o <track>. Un clic reproduce o pausa, un doble clic alterna la pantalla completa, un toque muestra u oculta los controles y un doble toque a cualquiera de los lados busca.

PropTipoPredeterminado
autoPlayInicia la reproducción al montar, salvo con movimiento reducido.
booleanfalse
playsInline
booleantrue
preload
"none" | "metadata" | "auto""metadata"
doubleTapSeekSegundos que salta un doble toque a cualquiera de los lados en pantallas táctiles. false lo desactiva.
number | false10
renderSustituye por otro elemento multimedia, como un elemento de vídeo HLS.
ReactElement | (props, state) => ReactElement<video>
AtributoDescripción
data-slot="video-player-content"Apunta al vídeo en CSS.
PropTipoPredeterminado
tooltipsMuestra la etiqueta y el atajo de cada control al pasar el cursor.
booleantrue
AtributoDescripción
data-slot="video-player-controls"Apunta a la barra de controles en CSS.
data-hiddenPresente mientras los controles overlay están ocultos.

Siempre ocupa su propia fila sobre los botones. Al pasar el cursor se muestra el tiempo bajo el puntero, y la pista más clara muestra lo que se ha cargado.

PropTipoPredeterminado
label
string"Seek"
onValueChange
(value: number, details) => void–
onValueCommitted
(value: number, details) => void–
disabled
booleanfalse
AtributoDescripción
data-slot="video-player-seek-bar"Apunta a la barra de búsqueda en CSS.
data-draggingPresente mientras arrastras la barra.
data-previewingPresente en el control mientras se muestra el tiempo del hover.
--video-player-bufferedLa parte cargada del vídeo, de 0 a 1.
--video-player-hoverLa posición del puntero a lo largo de la barra, de 0 a 1.
PropTipoPredeterminado
playLabel
string"Play"
pauseLabel
string"Pause"
replayLabel
string"Replay"
...propsTodas las props de Button, incluidas variant y size.
ButtonProps–
AtributoDescripción
data-slot="video-player-play-button"Apunta al botón en CSS.
data-state"paused", "playing" o "ended".
PropTipoPredeterminado
offsetSegundos que se salta. Los valores negativos retroceden.
number10
label
string"Forward 10 seconds"
...propsTodas las props de Button, incluidas variant y size.
ButtonProps–
AtributoDescripción
data-slot="video-player-seek-button"Apunta al botón en CSS.
data-direction"backward" o "forward".

Un botón de silencio con un slider que se abre al pasar el cursor o al enfocar. En pantallas táctiles solo se muestra el botón de silencio, ya que los teléfonos controlan el volumen con sus propios botones.

PropTipoPredeterminado
label
string"Volume"
muteLabel
string"Mute"
unmuteLabel
string"Unmute"
AtributoDescripción
data-slot="video-player-volume"Apunta al grupo en CSS.
data-slot="video-player-mute-button"El botón de silencio. También se exporta como VideoPlayerMuteButton.
data-stateEn el botón de silencio: "muted", "low" o "high".
PropTipoPredeterminado
type
"both" | "elapsed" | "remaining" | "duration""both"
AtributoDescripción
data-slot="video-player-time"Apunta al tiempo en CSS.
data-typeEl tipo actual.
PropTipoPredeterminado
rates
number[][0.5, 0.75, 1, 1.25, 1.5, 2]
label
string"Playback speed"
normalLabel
string"Normal"
AtributoDescripción
data-slot="video-player-playback-rate"Apunta al trigger del menú en CSS.

Pone a pantalla completa todo el reproductor, o el propio vídeo en iPhone. No renderiza nada donde la pantalla completa no está disponible.

PropTipoPredeterminado
enterLabel
string"Full screen"
exitLabel
string"Exit full screen"
AtributoDescripción
data-slot="video-player-fullscreen-button"Apunta al botón en CSS.
data-state"on" u "off".

No renderiza nada hasta que el vídeo tiene una pista de subtítulos.

PropTipoPredeterminado
label
string"Captions"
AtributoDescripción
data-slot="video-player-captions-button"Apunta al botón en CSS.
data-state"on" u "off".
data-slot="video-player-captions"El texto del subtítulo sobre el vídeo. data-lifted está presente mientras queda sobre los controles.

No renderiza nada en navegadores sin imagen en imagen.

PropTipoPredeterminado
enterLabel
string"Picture in picture"
exitLabel
string"Exit picture in picture"
AtributoDescripción
data-slot="video-player-pip-button"Apunta al botón en CSS.
data-state"on" u "off".

Llena el espacio libre de la fila de controles, empujando al final los controles que vienen después.

Devuelve el estado y las acciones del reproductor. Pasa un selector que devuelva un único valor.

const paused = useVideoPlayer((player) => player.paused)
const seek = useVideoPlayer((player) => player.seek)
PropTipoPredeterminado
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–