# Video Player

A themed media-chrome video player with composable controls.

## Installation

```bash
npx shadcn@latest add https://ui.jarv.is/r/video-player.json
```

[Registry JSON](https://ui.jarv.is/r/video-player.json)

## Preview

```tsx
import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControlBar,
  VideoPlayerMuteButton,
  VideoPlayerPlayButton,
  VideoPlayerSeekBackwardButton,
  VideoPlayerSeekForwardButton,
  VideoPlayerTimeDisplay,
  VideoPlayerTimeRange,
  VideoPlayerVolumeRange,
} from "@/components/ui/video-player";

export function Preview() {
  return (
    <div className="flex w-full max-w-2xl flex-col gap-3">
      <VideoPlayer className="aspect-video w-full overflow-hidden rounded-lg border bg-card">
        <VideoPlayerContent
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
          preload="metadata"
        />
        <VideoPlayerControlBar>
          <VideoPlayerPlayButton />
          <VideoPlayerSeekBackwardButton />
          <VideoPlayerTimeRange />
          <VideoPlayerTimeDisplay />
          <VideoPlayerSeekForwardButton />
          <VideoPlayerMuteButton />
          <VideoPlayerVolumeRange />
        </VideoPlayerControlBar>
      </VideoPlayer>
      <p className="text-center text-sm text-muted-foreground">Sample video from MDN Web Docs.</p>
    </div>
  );
}
```


## Source

### ui/video-player.tsx

```tsx
"use client";

import {
  MediaControlBar,
  MediaController,
  MediaMuteButton,
  MediaPlayButton,
  MediaSeekBackwardButton,
  MediaSeekForwardButton,
  MediaTimeDisplay,
  MediaTimeRange,
  MediaVolumeRange,
} from "media-chrome/react";
import type * as React from "react";

import { cn } from "@/lib/utils";

type VideoPlayerVariables = React.CSSProperties & Record<`--${string}`, string>;
type VideoPlayerContentProps = React.ComponentProps<"video"> & {
  captionsSrc?: string;
  captionsLabel?: string;
  captionsSrcLang?: string;
};

const videoPlayerVariables = {
  "--media-primary-color": "var(--primary)",
  "--media-secondary-color": "var(--background)",
  "--media-text-color": "var(--foreground)",
  "--media-background-color": "var(--background)",
  "--media-control-hover-background": "var(--accent)",
  "--media-font-family": "var(--font-sans)",
  "--media-live-button-icon-color": "var(--muted-foreground)",
  "--media-live-button-indicator-color": "var(--destructive)",
  "--media-range-track-background": "var(--border)",
} satisfies VideoPlayerVariables;
const emptyCaptionsSrc = "data:text/vtt,WEBVTT%0A%0A";

function VideoPlayer({ style, ...props }: React.ComponentProps<typeof MediaController>) {
  return <MediaController style={{ ...videoPlayerVariables, ...style }} {...props} />;
}

function VideoPlayerControlBar(props: React.ComponentProps<typeof MediaControlBar>) {
  return <MediaControlBar {...props} />;
}

function VideoPlayerTimeRange({
  className,
  ...props
}: React.ComponentProps<typeof MediaTimeRange>) {
  return <MediaTimeRange className={cn("p-2.5", className)} {...props} />;
}

function VideoPlayerTimeDisplay({
  className,
  ...props
}: React.ComponentProps<typeof MediaTimeDisplay>) {
  return <MediaTimeDisplay className={cn("p-2.5", className)} {...props} />;
}

function VideoPlayerVolumeRange({
  className,
  ...props
}: React.ComponentProps<typeof MediaVolumeRange>) {
  return <MediaVolumeRange className={cn("p-2.5", className)} {...props} />;
}

function VideoPlayerPlayButton({
  className,
  ...props
}: React.ComponentProps<typeof MediaPlayButton>) {
  return <MediaPlayButton aria-label="Play" className={cn("p-2.5", className)} {...props} />;
}

function VideoPlayerSeekBackwardButton({
  className,
  ...props
}: React.ComponentProps<typeof MediaSeekBackwardButton>) {
  return (
    <MediaSeekBackwardButton
      aria-label="Seek backward"
      className={cn("p-2.5", className)}
      {...props}
    />
  );
}

function VideoPlayerSeekForwardButton({
  className,
  ...props
}: React.ComponentProps<typeof MediaSeekForwardButton>) {
  return (
    <MediaSeekForwardButton
      aria-label="Seek forward"
      className={cn("p-2.5", className)}
      {...props}
    />
  );
}

function VideoPlayerMuteButton({
  className,
  ...props
}: React.ComponentProps<typeof MediaMuteButton>) {
  return <MediaMuteButton aria-label="Mute" className={cn("p-2.5", className)} {...props} />;
}

function VideoPlayerContent({
  className,
  children,
  captionsSrc = emptyCaptionsSrc,
  captionsLabel = "Captions",
  captionsSrcLang = "en",
  ...props
}: VideoPlayerContentProps) {
  return (
    <video slot="media" className={cn("my-0 block size-full", className)} {...props}>
      <track kind="captions" src={captionsSrc} label={captionsLabel} srcLang={captionsSrcLang} />
      {children}
    </video>
  );
}

export {
  VideoPlayer,
  VideoPlayerControlBar,
  VideoPlayerTimeRange,
  VideoPlayerTimeDisplay,
  VideoPlayerVolumeRange,
  VideoPlayerPlayButton,
  VideoPlayerSeekBackwardButton,
  VideoPlayerSeekForwardButton,
  VideoPlayerMuteButton,
  VideoPlayerContent,
};
```



## Usage

### Basic player

Use the video player wrappers to compose a media-chrome player with your app theme tokens.

```tsx
import { VideoPlayer, VideoPlayerContent, VideoPlayerControlBar } from "@/components/ui/video-player";

export function Example() {
  return (
    <VideoPlayer>
      <VideoPlayerContent src="/video.webm" />
      <VideoPlayerControlBar />
    </VideoPlayer>
  );
}
```

### Full controls

Compose only the controls your player needs. The wrappers forward props to `media-chrome`.

```tsx
import {
  VideoPlayer,
  VideoPlayerContent,
  VideoPlayerControlBar,
  VideoPlayerMuteButton,
  VideoPlayerPlayButton,
  VideoPlayerTimeDisplay,
  VideoPlayerTimeRange,
  VideoPlayerVolumeRange,
} from "@/components/ui/video-player";

export function TrainingVideo() {
  return (
    <VideoPlayer className="aspect-video overflow-hidden rounded-lg border">
      <VideoPlayerContent src="/training.webm" preload="metadata" />
      <VideoPlayerControlBar>
        <VideoPlayerPlayButton />
        <VideoPlayerTimeRange />
        <VideoPlayerTimeDisplay />
        <VideoPlayerMuteButton />
        <VideoPlayerVolumeRange />
      </VideoPlayerControlBar>
    </VideoPlayer>
  );
}
```

### Captions

Pass a captions track when the media has subtitles or transcripts.

```tsx
import { VideoPlayer, VideoPlayerContent, VideoPlayerControlBar } from "@/components/ui/video-player";

export function CaptionedVideo() {
  return (
    <VideoPlayer className="aspect-video">
      <VideoPlayerContent
        src="/launch.webm"
        captionsSrc="/captions/launch.vtt"
        captionsLabel="English"
        captionsSrcLang="en"
      />
      <VideoPlayerControlBar />
    </VideoPlayer>
  );
}
```

### Theme overrides

Override media-chrome CSS variables through `style` for one-off branded players.

```tsx
import type { CSSProperties } from "react";

import { VideoPlayer, VideoPlayerContent, VideoPlayerControlBar } from "@/components/ui/video-player";

type VideoTheme = CSSProperties & Record<`--${string}`, string>;

const playerTheme = {
  "--media-primary-color": "white",
  "--media-secondary-color": "black",
  "--media-control-hover-background": "rgb(255 255 255 / 0.16)",
} satisfies VideoTheme;

export function BrandedVideo() {
  return (
    <VideoPlayer className="aspect-video overflow-hidden rounded-lg" style={playerTheme}>
      <VideoPlayerContent src="/demo.webm" />
      <VideoPlayerControlBar />
    </VideoPlayer>
  );
}
```

### Seek controls

Add skip controls for long-form content where users need quick navigation.

```tsx
import {
  VideoPlayerControlBar,
  VideoPlayerSeekBackwardButton,
  VideoPlayerSeekForwardButton,
} from "@/components/ui/video-player";

export function PlaybackControls() {
  return (
    <VideoPlayerControlBar>
      <VideoPlayerSeekBackwardButton seekOffset={10} />
      <VideoPlayerSeekForwardButton seekOffset={30} />
    </VideoPlayerControlBar>
  );
}
```

