HextaUI

Video player

Ein Videoplayer mit scrubbarer Suchleiste, automatisch ausblendenden Steuerelementen, Tastenkürzeln, Geschwindigkeit, Bild-in-Bild und Vollbild.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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

Leiste

variant="bar" setzt die Steuerelemente unter das Bild auf die Seitenfläche. Sie verbergen oder verdecken das Video nie.

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

Nutze nur die Teile, die du brauchst. tooltips={false} schaltet die Hover-Hinweise ab, und type="remaining" zählt herunter statt hoch.

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

Such-Offsets und Geschwindigkeiten

offset legt fest, wie weit jeder Such-Button springt, und rates die Geschwindigkeiten im 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>
  )
}

Seitenweite Tastenkürzel

Tastenkürzel funktionieren, solange der Fokus im Player ist. globalShortcuts lauscht auch auf der Seite, aber nie, während du in ein Feld tippst, einen Button nutzt oder ein Menü oder Dialog offen ist. Nutze es bei einem Player pro Seite.

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

Untertitel

Füge ein <track> und VideoPlayerCaptionsButton hinzu. Untertitel werden vom Player gezeichnet und rücken daher nach oben, solange die Steuerelemente angezeigt werden, statt dahinter zu verschwinden. Ein Cross-Origin-Track braucht crossOrigin am Video.

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

Eigene Steuerelemente

useVideoPlayer liest Zustand und Aktionen aus jeder Komponente im Player. Wähle nur aus, was du nutzt, damit die Komponente nur neu rendert, wenn sich dieser Wert ändert.

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

Fehler

Schlägt die Quelle fehl, zeigt der Player errorMessage, sagt sie an und deaktiviert die Steuerelemente, die nicht funktionieren können.

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

Rechts nach links

Labels folgen der Sprache der Seite. Zeitleiste und Transportsteuerung bleiben von links nach rechts, wie bei Medienplayern der Plattformen.

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

Diese funktionieren, solange der Fokus irgendwo im Player ist, oder mit globalShortcuts auf der Seite. Sie werden übersprungen, solange eine Modifier-Taste gehalten wird oder der Fokus in einem Textfeld ist.

TasteAktion
SpaceKStartet oder pausiert.
JSpringt 10 Sekunden zurück.
LSpringt 10 Sekunden vor.
←→Springt 5 Sekunden zurück oder vor. In der Suchleiste springt Shift um 10.
↑↓Stellt die Lautstärke um 5% lauter oder leiser.
MSchaltet stumm oder hebt es auf.
CSchaltet Untertitel ein oder aus, wenn das Video welche hat.
FWechselt in den Vollbildmodus oder verlässt ihn.
IÖffnet oder schließt Bild-in-Bild, wo unterstützt.
Shift+.Shift+,Beschleunigt oder verlangsamt die Wiedergabe.
0–9Springt zu 0% bis 90% des Videos.
HomeEndSpringt zum Anfang oder zum Ende.
  • Der Player ist ein beschrifteter Bereich. Jeder Button hat einen Namen, der seinem Zustand folgt (Play, Pause, Replay), und einen Tooltip mit seinem Tastenkürzel.
  • Suchleiste und Lautstärke sind Slider. Die Suchleiste liest ihren Wert als „1 Minute 5 Sekunden von 3 Minuten“.
  • Aktionen aus Tastenkürzeln und Videoklicks werden höflich angesagt, etwa „Paused“ oder „Volume 40%“. Ladefehler werden als Alert angesagt.
  • In der Overlay-Variante blenden Steuerelemente nach 2,5 Sekunden Wiedergabe ohne Zeigerbewegung aus. Sie bleiben sichtbar, solange pausiert ist, du hoverst oder sie per Tastatur nutzt und ein Menü offen ist.
  • Auf Touchscreens zeigt oder verbirgt ein Tippen die Steuerelemente, und ein Doppeltippen im linken oder rechten Drittel springt 10 Sekunden zurück oder vor. Tippe weiter, um jedes Mal 10 Sekunden hinzuzufügen.
  • Der Untertitel-Button ist ein Toggle mit aria-pressed. Er wählt den zuletzt genutzten Track, dann einen in der Sprache des Browsers, dann den ersten.
  • Bei reduzierter Bewegung blenden Steuerelemente und Feedback ein und aus, ohne sich zu bewegen oder zu skalieren.

Suchleiste und Lautstärke sind auf dem Base UI Slider gebaut, die Buttons auf Button, Tooltip und Dropdown menu von HextaUI.

PropTypStandard
variantoverlay lässt automatisch ausblendende Steuerelemente über dem Video schweben. bar setzt sie darunter.
"overlay" | "bar""overlay"
shortcutsTastenkürzel, solange der Fokus im Player ist.
booleantrue
globalShortcutsLausche auf Tastenkürzel auch auf der ganzen Seite.
booleanfalse
errorMessage
ReactNode"This video can’t be played."
AttributBeschreibung
data-slot="video-player"Das Root in CSS ansprechen.
data-variantDie aktuelle Variante.
data-controls"visible" oder "hidden". Der Cursor verschwindet mit den Steuerelementen.
data-fullscreenVorhanden, solange der Player im Vollbild ist.
aria-busyGesetzt, solange die Wiedergabe auf Daten wartet.

Das <video>-Element. Es nimmt alle Video-Attribute und <source>- oder <track>-Kinder. Ein Klick startet oder pausiert, ein Doppelklick schaltet Vollbild um, ein Tippen zeigt oder verbirgt die Steuerelemente, und ein Doppeltippen an beiden Seiten springt.

PropTypStandard
autoPlayStartet die Wiedergabe beim Mount, außer bei reduzierter Bewegung.
booleanfalse
playsInline
booleantrue
preload
"none" | "metadata" | "auto""metadata"
doubleTapSeekSekunden, die ein Doppeltippen an beiden Seiten auf Touchscreens springt. false schaltet es ab.
number | false10
renderTausche ein anderes Media-Element ein, etwa ein HLS-Video-Element.
ReactElement | (props, state) => ReactElement<video>
AttributBeschreibung
data-slot="video-player-content"Das Video per CSS ansprechen.
PropTypStandard
tooltipsZeige Label und Tastenkürzel jedes Steuerelements bei Hover.
booleantrue
AttributBeschreibung
data-slot="video-player-controls"Die Steuerleiste per CSS ansprechen.
data-hiddenVorhanden, solange Overlay-Steuerelemente verborgen sind.

Belegt immer eine eigene Zeile über den Buttons. Hover zeigt die Zeit unter dem Zeiger, und die hellere Spur zeigt, was geladen ist.

PropTypStandard
label
string"Seek"
onValueChange
(value: number, details) => void–
onValueCommitted
(value: number, details) => void–
disabled
booleanfalse
AttributBeschreibung
data-slot="video-player-seek-bar"Die Suchleiste per CSS ansprechen.
data-draggingVorhanden, solange du scrubbst.
data-previewingVorhanden am Steuerelement, solange die Hover-Zeit angezeigt wird.
--video-player-bufferedDer geladene Teil des Videos, von 0 bis 1.
--video-player-hoverDie Zeigerposition entlang der Leiste, von 0 bis 1.
PropTypStandard
playLabel
string"Play"
pauseLabel
string"Pause"
replayLabel
string"Replay"
...propsJede Button-Prop, auch variant und size.
ButtonProps–
AttributBeschreibung
data-slot="video-player-play-button"Den Button per CSS ansprechen.
data-state"paused", "playing" oder "ended".
PropTypStandard
offsetSekunden, die gesprungen wird. Negative Werte gehen zurück.
number10
label
string"Forward 10 seconds"
...propsJede Button-Prop, auch variant und size.
ButtonProps–
AttributBeschreibung
data-slot="video-player-seek-button"Den Button per CSS ansprechen.
data-direction"backward" oder "forward".

Ein Stummschalten-Button mit einem Slider, der bei Hover oder Fokus öffnet. Auf Touchscreens erscheint nur der Stummschalten-Button, da Smartphones die Lautstärke mit eigenen Tasten steuern.

PropTypStandard
label
string"Volume"
muteLabel
string"Mute"
unmuteLabel
string"Unmute"
AttributBeschreibung
data-slot="video-player-volume"Die Gruppe in CSS ansprechen.
data-slot="video-player-mute-button"Der Stummschalten-Button. Auch als VideoPlayerMuteButton exportiert.
data-stateAm Stummschalten-Button: "muted", "low" oder "high".
PropTypStandard
type
"both" | "elapsed" | "remaining" | "duration""both"
AttributBeschreibung
data-slot="video-player-time"Die Zeit per CSS ansprechen.
data-typeDer aktuelle Typ.
PropTypStandard
rates
number[][0.5, 0.75, 1, 1.25, 1.5, 2]
label
string"Playback speed"
normalLabel
string"Normal"
AttributBeschreibung
data-slot="video-player-playback-rate"Den Menü-Trigger per CSS ansprechen.

Schaltet den ganzen Player in den Vollbildmodus, auf dem iPhone das Video selbst. Rendert nichts, wo Vollbild nicht verfügbar ist.

PropTypStandard
enterLabel
string"Full screen"
exitLabel
string"Exit full screen"
AttributBeschreibung
data-slot="video-player-fullscreen-button"Den Button per CSS ansprechen.
data-state"on" oder "off".

Rendert nichts, bis das Video einen Untertitel- oder Captions-Track hat.

PropTypStandard
label
string"Captions"
AttributBeschreibung
data-slot="video-player-captions-button"Den Button per CSS ansprechen.
data-state"on" oder "off".
data-slot="video-player-captions"Der Untertiteltext auf dem Video. data-lifted ist vorhanden, solange er über den Steuerelementen sitzt.

Rendert nichts in Browsern ohne Bild-in-Bild.

PropTypStandard
enterLabel
string"Picture in picture"
exitLabel
string"Exit picture in picture"
AttributBeschreibung
data-slot="video-player-pip-button"Den Button per CSS ansprechen.
data-state"on" oder "off".

Füllt den freien Platz in der Steuerzeile und schiebt die folgenden Steuerelemente ans Ende.

Gibt Zustand und Aktionen des Players zurück. Übergib einen Selector, der einen einzelnen Wert zurückgibt.

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