Skip to main content
A skin packages UI components and their styles into a single component. Video.js ships with pre-built skins you can use as-is or eject to customize fully.

Packaged vs. ejected

PackagedEjected
UsageSingle componentMany individual components
CustomizationLimited (CSS variables)Complete
Design updatesAuto-applied on version bumpApplied manually
Start with a packaged skin. Eject when you need structural changes.

CSS custom properties

Packaged skins expose CSS custom properties for colors, spacing, and typography. Override them in your own stylesheet:
styles.css
/* Override the default skin's color tokens */
.my-player {
  --media-primary-color: #ff6b35;
  --media-background-color: #1a1a2e;
  --media-control-bar-height: 48px;
  --media-border-radius: 12px;
}
Apply overrides to the player container’s class so they’re scoped per-player instance and don’t bleed into other players on the page.

Eject a skin

Ejecting copies all the internal components from a skin into your own project. You then own the markup, CSS, and structure — future Video.js updates won’t overwrite your customizations. The customize skins reference page has copy-paste-ready ejected implementations for:
  • Default video skin
  • Default audio skin
  • Minimal video skin
  • Minimal audio skin

Build a custom skin from scratch

React

Use Player.Container as the layout wrapper, then compose UI components inside it:
MyVideoSkin.tsx
import {
  PlayButton,
  MuteButton,
  FullscreenButton,
  TimeSlider,
  Controls,
} from '@videojs/react';
import type { ReactNode } from 'react';
import { Player } from './player';
import './my-skin.css';

export function MyVideoSkin({ children }: { children: ReactNode }) {
  return (
    <Player.Container className="my-skin">
      {children}

      <Controls.Root className="my-skin__controls">
        <PlayButton
          render={(props, state) => (
            <button {...props} className="my-skin__play">
              {state.paused ? '▶' : '⏸'}
            </button>
          )}
        />

        <TimeSlider.Root className="my-skin__seek">
          <TimeSlider.Track>
            <TimeSlider.Fill />
            <TimeSlider.Buffer />
          </TimeSlider.Track>
          <TimeSlider.Thumb />
        </TimeSlider.Root>

        <MuteButton
          render={(props, state) => (
            <button {...props} className="my-skin__mute">
              {state.muted ? '🔇' : '🔊'}
            </button>
          )}
        />

        <FullscreenButton
          render={(props, state) => (
            <button {...props} className="my-skin__fullscreen">
              {state.fullscreen ? 'Exit' : 'Fullscreen'}
            </button>
          )}
        />
      </Controls.Root>
    </Player.Container>
  );
}
Then use it:
App.tsx
import { Video } from '@videojs/react/video';
import { Player } from './player';
import { MyVideoSkin } from './MyVideoSkin';

export function App() {
  return (
    <Player.Provider>
      <MyVideoSkin>
        <Video src="movie.mp4" />
      </MyVideoSkin>
    </Player.Provider>
  );
}

HTML

Use <media-container> as the layout wrapper and compose individual UI elements inside it:
import '@videojs/html/video/player';
import '@videojs/html/media/container';
import '@videojs/html/ui/play-button';
import '@videojs/html/ui/mute-button';
import '@videojs/html/ui/fullscreen-button';
import '@videojs/html/ui/time-slider';
<video-player>
  <media-container class="my-skin">
    <video slot="media" src="movie.mp4"></video>

    <div class="my-skin__controls">
      <media-play-button class="my-skin__play">
        <span class="show-when-paused">Play</span>
        <span class="show-when-playing">Pause</span>
      </media-play-button>

      <media-time-slider class="my-skin__seek"></media-time-slider>

      <media-mute-button class="my-skin__mute">
        <span class="show-when-muted">Unmute</span>
        <span class="show-when-unmuted">Mute</span>
      </media-mute-button>

      <media-fullscreen-button class="my-skin__fullscreen"></media-fullscreen-button>
    </div>
  </media-container>
</video-player>

Theming with data-* attributes

Video.js elements reflect state as data-* attributes. Use CSS attribute selectors to drive all visual state changes — no JavaScript required:
my-skin.css
/* Play button: show correct icon based on playback state */
.my-skin__play .show-when-paused  { display: none; }
.my-skin__play .show-when-playing { display: none; }
.my-skin__play[data-paused] .show-when-paused   { display: inline; }
.my-skin__play:not([data-paused]) .show-when-playing { display: inline; }

/* Mute button: show correct icon */
.my-skin__mute .show-when-muted   { display: none; }
.my-skin__mute .show-when-unmuted { display: none; }
.my-skin__mute[data-muted] .show-when-muted     { display: inline; }
.my-skin__mute:not([data-muted]) .show-when-unmuted { display: inline; }

/* Controls: fade in on hover */
.my-skin__controls {
  opacity: 0;
  transition: opacity 0.2s;
}
.my-skin:hover .my-skin__controls,
.my-skin[data-paused] .my-skin__controls {
  opacity: 1;
}
Common data-* attributes exposed by Video.js elements:
AttributeSet when
data-pausedMedia is paused
data-mutedVolume is muted
data-fullscreenPlayer is in fullscreen
data-availabilityFeature availability (available, unavailable, unsupported)
data-paused on <media-play-button>Media is paused
data-direction on <media-seek-button>forward or backward

Render prop pattern in React

Every headless UI component accepts a render prop. The render function receives two arguments: HTML props to spread onto the element, and the current state relevant to that component:
<PlayButton
  render={(props, state) => (
    // `props` — accessibility attributes, event handlers, ref, etc.
    // `state` — { paused, ended, ... }
    <button {...props} className={state.paused ? 'btn-play' : 'btn-pause'}>
      {state.paused ? 'Play' : 'Pause'}
    </button>
  )}
/>
Always spread props onto your root element. It contains aria-* attributes, tabIndex, role, and event handlers that are required for accessibility.

Next steps

Custom features

Control which features and state your player includes.

Media sources

Swap in HLS, DASH, and other media providers.