HextaUI

Video player

Un lecteur vidéo avec une barre de recherche déplaçable, des contrôles qui se masquent automatiquement, des raccourcis clavier, la vitesse, l'image dans l'image et le plein écran.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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

Barre

variant="bar" place les contrôles sous l'image, sur la surface de la page. Ils ne masquent ni ne recouvrent jamais la vidéo.

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

Minimal

N'utilisez que les parties dont vous avez besoin. tooltips={false} désactive les indices au survol, et type="remaining" décompte au lieu de compter.

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

Décalages de recherche et vitesses

offset définit la distance de saut de chaque bouton de recherche, et rates les vitesses du 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>
  )
}

Raccourcis sur toute la page

Les raccourcis fonctionnent tant que le focus est dans le lecteur. globalShortcuts écoute aussi sur la page, mais jamais pendant que vous saisissez dans un champ, utilisez un bouton ou avez un menu ou un dialog ouvert. Utilisez-le pour un seul lecteur par page.

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

Sous-titres

Ajoutez un <track> et VideoPlayerCaptionsButton. Les sous-titres sont dessinés par le lecteur : ils remontent donc quand les contrôles s'affichent au lieu de se cacher derrière. Une piste d'origine croisée nécessite crossOrigin sur la vidéo.

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

Contrôles personnalisés

useVideoPlayer lit l'état et les actions depuis n'importe quel composant dans le lecteur. Ne sélectionnez que ce que vous utilisez, pour que le composant ne se re-rende que quand cette valeur change.

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

Erreur

Quand la source échoue, le lecteur affiche errorMessage, l'annonce et désactive les contrôles qui ne peuvent pas fonctionner.

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 droite à gauche

Les labels suivent la langue de la page. La timeline et les contrôles de lecture restent de gauche à droite, comme dans les lecteurs multimédias des plateformes.

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

Ils fonctionnent tant que le focus est n'importe où dans le lecteur, ou sur la page avec globalShortcuts. Ils sont ignorés quand une touche de modification est maintenue ou que le focus est dans un champ de texte.

ToucheAction
SpaceKLance ou met en pause.
JRecule de 10 secondes.
LAvance de 10 secondes.
←→Recule ou avance de 5 secondes. Sur la barre de recherche, Shift saute de 10.
↑↓Augmente ou diminue le volume de 5%.
MCoupe ou rétablit le son.
CActive ou désactive les sous-titres, quand la vidéo en a.
FEntre en plein écran ou le quitte.
IOuvre ou ferme l'image dans l'image, là où elle est prise en charge.
Shift+.Shift+,Accélère ou ralentit la lecture.
0–9Saute de 0% à 90% de la vidéo.
HomeEndSaute au début ou à la fin.
  • Le lecteur est une région avec un label. Chaque bouton a un nom qui suit son état (Play, Pause, Replay) et un tooltip avec son raccourci.
  • La barre de recherche et le volume sont des sliders. La barre de recherche lit sa valeur comme « 1 minute 5 secondes sur 3 minutes ».
  • Les actions des raccourcis et des clics sur la vidéo sont annoncées poliment, par exemple « Paused » ou « Volume 40% ». Les erreurs de chargement sont annoncées comme une alerte.
  • Dans la variante overlay, les contrôles s'estompent après 2,5 secondes de lecture sans mouvement du pointeur. Ils restent visibles en pause, pendant que vous les survolez ou les utilisez au clavier, et tant qu'un menu est ouvert.
  • Sur écran tactile, un tap affiche ou masque les contrôles et un double tap sur le tiers gauche ou droit recule ou avance de 10 secondes. Continuez à taper pour ajouter 10 secondes à chaque fois.
  • Le bouton de sous-titres est un toggle avec aria-pressed. Il choisit la dernière piste utilisée, puis une dans la langue du navigateur, puis la première.
  • Avec la réduction des animations, les contrôles et le retour visuel apparaissent en fondu sans mouvement ni changement d'échelle.

La barre de recherche et le volume reposent sur le slider de Base UI, et les boutons sur Button, Tooltip et Dropdown menu de HextaUI.

PropTypePar défaut
variantoverlay fait flotter des contrôles à masquage automatique sur la vidéo. bar les place en dessous.
"overlay" | "bar""overlay"
shortcutsRaccourcis clavier tant que le focus est dans le lecteur.
booleantrue
globalShortcutsÉcoute aussi les raccourcis sur toute la page.
booleanfalse
errorMessage
ReactNode"This video can’t be played."
AttributDescription
data-slot="video-player"Ciblez la racine en CSS.
data-variantLa variante actuelle.
data-controls"visible" ou "hidden". Le curseur se masque avec les contrôles.
data-fullscreenPrésent tant que le lecteur est en plein écran.
aria-busyDéfini tant que la lecture attend des données.

L'élément <video>. Il accepte tous les attributs de video, et des enfants <source> ou <track>. Un clic lance ou met en pause, un double clic bascule le plein écran, un tap affiche ou masque les contrôles et un double tap de chaque côté fait une recherche.

PropTypePar défaut
autoPlayLance la lecture au montage, sauf avec la réduction des animations.
booleanfalse
playsInline
booleantrue
preload
"none" | "metadata" | "auto""metadata"
doubleTapSeekSecondes sautées par un double tap de chaque côté sur écran tactile. false le désactive.
number | false10
renderRemplace par un autre élément média, comme un élément vidéo HLS.
ReactElement | (props, state) => ReactElement<video>
AttributDescription
data-slot="video-player-content"Cible la vidéo en CSS.
PropTypePar défaut
tooltipsAffiche le label et le raccourci de chaque contrôle au survol.
booleantrue
AttributDescription
data-slot="video-player-controls"Cible la barre de contrôle en CSS.
data-hiddenPrésent tant que les contrôles overlay sont masqués.

Occupe toujours sa propre ligne au-dessus des boutons. Le survol affiche l'heure sous le pointeur, et la piste plus claire montre ce qui est chargé.

PropTypePar défaut
label
string"Seek"
onValueChange
(value: number, details) => void–
onValueCommitted
(value: number, details) => void–
disabled
booleanfalse
AttributDescription
data-slot="video-player-seek-bar"Cible la barre de recherche en CSS.
data-draggingPrésent pendant que vous déplacez le curseur de lecture.
data-previewingPrésent sur le contrôle tant que l'heure de survol est affichée.
--video-player-bufferedLa partie chargée de la vidéo, de 0 à 1.
--video-player-hoverLa position du pointeur le long de la barre, de 0 à 1.
PropTypePar défaut
playLabel
string"Play"
pauseLabel
string"Pause"
replayLabel
string"Replay"
...propsToutes les props de Button, y compris variant et size.
ButtonProps–
AttributDescription
data-slot="video-player-play-button"Cible le bouton en CSS.
data-state"paused", "playing" ou "ended".
PropTypePar défaut
offsetSecondes à sauter. Les valeurs négatives reculent.
number10
label
string"Forward 10 seconds"
...propsToutes les props de Button, y compris variant et size.
ButtonProps–
AttributDescription
data-slot="video-player-seek-button"Cible le bouton en CSS.
data-direction"backward" ou "forward".

Un bouton muet avec un slider qui s'ouvre au survol ou au focus. Sur écran tactile, seul le bouton muet s'affiche, car les téléphones règlent le volume avec leurs propres boutons.

PropTypePar défaut
label
string"Volume"
muteLabel
string"Mute"
unmuteLabel
string"Unmute"
AttributDescription
data-slot="video-player-volume"Ciblez le groupe en CSS.
data-slot="video-player-mute-button"Le bouton muet. Aussi exporté sous le nom VideoPlayerMuteButton.
data-stateSur le bouton muet : "muted", "low" ou "high".
PropTypePar défaut
type
"both" | "elapsed" | "remaining" | "duration""both"
AttributDescription
data-slot="video-player-time"Cible l'heure en CSS.
data-typeLe type courant.
PropTypePar défaut
rates
number[][0.5, 0.75, 1, 1.25, 1.5, 2]
label
string"Playback speed"
normalLabel
string"Normal"
AttributDescription
data-slot="video-player-playback-rate"Cible le déclencheur du menu en CSS.

Met tout le lecteur en plein écran, ou la vidéo elle-même sur iPhone. Ne rend rien là où le plein écran n'est pas disponible.

PropTypePar défaut
enterLabel
string"Full screen"
exitLabel
string"Exit full screen"
AttributDescription
data-slot="video-player-fullscreen-button"Cible le bouton en CSS.
data-state"on" ou "off".

Ne rend rien tant que la vidéo n'a pas de piste de sous-titres.

PropTypePar défaut
label
string"Captions"
AttributDescription
data-slot="video-player-captions-button"Cible le bouton en CSS.
data-state"on" ou "off".
data-slot="video-player-captions"Le texte des sous-titres sur la vidéo. data-lifted est présent tant qu'il se trouve au-dessus des contrôles.

Ne rend rien dans les navigateurs sans image dans l'image.

PropTypePar défaut
enterLabel
string"Picture in picture"
exitLabel
string"Exit picture in picture"
AttributDescription
data-slot="video-player-pip-button"Cible le bouton en CSS.
data-state"on" ou "off".

Remplit l'espace libre de la rangée de contrôles, en repoussant les contrôles suivants vers la fin.

Renvoie l'état et les actions du lecteur. Passez un sélecteur qui renvoie une seule valeur.

const paused = useVideoPlayer((player) => player.paused)
const seek = useVideoPlayer((player) => player.seek)
PropTypePar défaut
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–