# Build your own skin

Create a skin project, preview it locally, and compose your own player interface from Video.js UI components

A skin is the player interface: markup that arranges Video.js UI components inside the player, plus the styles that give it a look. The components handle playback state, keyboard support, and accessibility, so building your own skin is mostly a layout and design job. [Skins](https://videojs.org/docs/framework/react/guides/skins) explains how skins fit into Video.js.

If a packaged skin is already close to what you want, [Customize skins](https://videojs.org/docs/framework/react/guides/customize-skins) is less work.

## Create a skin project

The quickest way to start is a standalone skin project:

**npm**

```bash
npx @videojs/cli skin create
```

**pnpm**

```bash
pnpm dlx @videojs/cli skin create
```

**yarn**

```bash
npx @videojs/cli skin create --package-manager yarn
```

**bun**

```bash
bunx @videojs/cli skin create
```

The command asks for a name, a framework (React or HTML), a styling approach (Tailwind or CSS), a starting skin, and a preset (video, audio, live video, or live audio). The starting skin defaults to [Scaffold](https://videojs.org/docs/framework/react/reference/components/video-scaffold-skin): every control wired up, with minimal styling and no transitions, so there’s little to undo.

The project goes in a folder named after the skin, so a skin called “My Skin” goes in `my-skin`. Open it and start the preview:

**npm**

```bash
cd my-skin
npm run dev
```

**pnpm**

```bash
cd my-skin
pnpm dev
```

**yarn**

```bash
cd my-skin
yarn dev
```

**bun**

```bash
cd my-skin
bun dev
```

The project has two parts:

- **`src/`** holds the skin and nothing else. Edit it however you like, including its markup, class names, theme tokens, and stylesheets.
- **`apps/preview/`** is a workbench that plays your skin. Use it to try different player sizes and aspect ratios, light and dark color schemes, right-to-left text, every built-in language, and playback settings such as autoplay and loop.

When you want to share it, deploy the workbench to Vercel, Netlify, or Cloudflare Pages:

**npm**

```bash
npm run deploy
```

**pnpm**

```bash
pnpm run deploy
```

**yarn**

```bash
yarn run deploy
```

**bun**

```bash
bun run deploy
```

Add the host’s name to skip the question, as in `npm run deploy vercel`. With pnpm, keep the `run`, since `pnpm deploy` is a built-in pnpm command. When the skin is done, move `src/` into your app.

### Build it with a coding agent

If you work with a coding agent, such as Claude Code, Codex, or Cursor, it can do all of this for you. Install the Video.js skills, which include a skill for building skins:

```bash
npx @videojs/cli agents skills
```

Then ask your agent something like “Help me design my own Video.js skin for our podcast player.” It asks for the same choices as the command, creates the project, starts the workbench, and agrees a design with you before styling. It can also match an existing player’s look from a screenshot or a link, then checks its work against the reference and for accessibility before handing back. [Build with AI](https://videojs.org/docs/framework/react/guides/build-with-ai) covers installing the skills in each agent.

## Other starting points

- **Inside an existing app.** Add a skin’s source with [Install with Shadcn](https://videojs.org/docs/guides/installation/shadcn?framework=react) and pick **Scaffold** as the skin.
- **From nothing.** Put a [Container](https://videojs.org/docs/framework/react/reference/components/player-container) inside your player and add the components you want, as in the example below.

No path requires Scaffold’s structure, its theme tokens, or any particular CSS approach.

## A minimal skin

This example shows how a skin fits together without Scaffold’s extra parts. It has a play button, a timeline, the current time, mute, fullscreen, keyboard shortcuts, screen reader announcements, and an error dialog. The player provides state, the Container is the skin’s root, and the media element goes inside the Container next to the controls.

**VideoSkin.tsx**

```tsx
import {
  Container,
  Controls,
  ErrorDialog,
  FullscreenButton,
  Hotkey,
  MuteButton,
  PlayButton,
  StatusAnnouncer,
  Time,
  TimeSlider,
} from '@videojs/react';
import type { ReactNode } from 'react';
import './video-skin.css';

export function VideoSkin({ children }: { children: ReactNode }) {
  return (
    <Container className="video-skin">
      {children}

      <Controls.Root>
        <Controls.Content className="video-skin-controls">
          <TimeSlider.Root className="video-skin-time-slider">
            <TimeSlider.Track className="video-skin-track">
              <TimeSlider.Buffer className="video-skin-buffer" />
              <TimeSlider.Fill className="video-skin-fill" />
            </TimeSlider.Track>
            <TimeSlider.Thumb className="video-skin-thumb" />
          </TimeSlider.Root>

          <Controls.Group className="video-skin-controls-group" aria-label="Playback controls">
            <PlayButton
              className="video-skin-play"
              render={(props, state) => <button {...props}>{state.paused ? 'Play' : 'Pause'}</button>}
            />
            <Time.Value type="current" />
            <MuteButton
              className="video-skin-mute"
              render={(props, state) => <button {...props}>{state.muted ? 'Unmute' : 'Mute'}</button>}
            />
            <FullscreenButton
              className="video-skin-fullscreen"
              render={(props, state) => (
                <button {...props}>{state.fullscreen ? 'Exit fullscreen' : 'Fullscreen'}</button>
              )}
            />
          </Controls.Group>
        </Controls.Content>
      </Controls.Root>

      <Hotkey keys="Space" action="togglePaused" />
      <Hotkey keys="m" action="toggleMuted" />
      <Hotkey keys="f" action="toggleFullscreen" />
      <Hotkey keys="ArrowLeft" action="seekStep" value={-5} />
      <Hotkey keys="ArrowRight" action="seekStep" value={5} />

      <StatusAnnouncer />

      <ErrorDialog.Root>
        <ErrorDialog.Popup className="video-skin-error">
          <ErrorDialog.Title />
          <ErrorDialog.Description />
          <ErrorDialog.Close />
        </ErrorDialog.Popup>
      </ErrorDialog.Root>
    </Container>
  );
}
```

Use it the same way as a packaged skin, with the media as its child:

**Player.tsx**

```tsx
import { createPlayer } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import { VideoSkin } from './VideoSkin';

const { Player } = createPlayer({ features: videoFeatures });

export function VideoPlayer() {
  return (
    <Player>
      <VideoSkin>
        <Video src="video.mp4" playsInline />
      </VideoSkin>
    </Player>
  );
}
```

`videoFeatures` gives the player the state these controls need. Some controls, such as Cast or Quality, depend on features that not every preset includes. Each control’s reference page names the feature it needs.

This CSS places the controls over the video, hides them when the viewer is idle, draws the timeline, and keeps the error dialog hidden until playback fails:

**video-skin.css**

```css
.video-skin {
  position: relative;
  display: block;
}

.video-skin video {
  display: block;
  width: 100%;
}

.video-skin-controls {
  position: absolute;
  inset: auto 0 0;
  padding: 12px;
  background: linear-gradient(to top, rgb(0 0 0 / 0.7), transparent);
  color: white;
  transition: opacity 0.25s;
}

/* Keep controls visible while something inside them has keyboard focus. */
.video-skin-controls:not([data-visible]):not(:has(:focus-visible)) {
  opacity: 0;
}

.video-skin-controls-group {
  display: flex;
  gap: 8px;
  align-items: center;
}

/* The slider sets its progress as CSS custom properties. */
.video-skin-time-slider {
  position: relative;
  display: flex;
  align-items: center;
  height: 20px;
  cursor: pointer;
}

.video-skin-track {
  position: relative;
  width: 100%;
  height: 4px;
  background: rgb(255 255 255 / 0.3);
  border-radius: 9999px;
}

.video-skin-buffer,
.video-skin-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  border-radius: inherit;
}

.video-skin-buffer {
  width: var(--media-slider-buffer);
  background: rgb(255 255 255 / 0.4);
}

.video-skin-fill {
  width: var(--media-slider-fill);
  background: white;
}

.video-skin-thumb {
  position: absolute;
  left: var(--media-slider-fill);
  width: 12px;
  height: 12px;
  background: white;
  border-radius: 50%;
  translate: -50% 0;
}

.video-skin-error {
  position: absolute;
  inset: 0;
  display: grid;
  gap: 12px;
  place-content: center;
  padding: 24px;
  background: rgb(0 0 0 / 0.85);
  color: white;
  text-align: center;
}

.video-skin-error:not([data-open]) {
  display: none;
}
```

From here, add the components your design needs.

## Pick the components

Each skin is made of the same building blocks. Use the ones your design needs; their reference pages cover props, state, data attributes, and examples.

| Purpose | Components |
| --- | --- |
| Player surface, fullscreen and activity target | [Container](https://videojs.org/docs/framework/react/reference/components/player-container) |
| Show and hide controls with user activity | [Controls](https://videojs.org/docs/framework/react/reference/components/controls) |
| Playback buttons | [Play button](https://videojs.org/docs/framework/react/reference/components/play-button), [Seek button](https://videojs.org/docs/framework/react/reference/components/seek-button), [Mute button](https://videojs.org/docs/framework/react/reference/components/mute-button), [Playback rate button](https://videojs.org/docs/framework/react/reference/components/playback-rate-button), [Live button](https://videojs.org/docs/framework/react/reference/components/live-button) |
| Screen and device buttons | [Fullscreen button](https://videojs.org/docs/framework/react/reference/components/fullscreen-button), [Picture-in-picture button](https://videojs.org/docs/framework/react/reference/components/pip-button), [AirPlay button](https://videojs.org/docs/framework/react/reference/components/airplay-button), [Cast button](https://videojs.org/docs/framework/react/reference/components/cast-button), [Captions button](https://videojs.org/docs/framework/react/reference/components/captions-button) |
| Timeline and volume | [Time slider](https://videojs.org/docs/framework/react/reference/components/time-slider), [Volume slider](https://videojs.org/docs/framework/react/reference/components/volume-slider), [Volume popover](https://videojs.org/docs/framework/react/reference/components/volume-popover), [Thumbnail](https://videojs.org/docs/framework/react/reference/components/thumbnail), [Time](https://videojs.org/docs/framework/react/reference/components/time) |
| Menus and popups | [Menu](https://videojs.org/docs/framework/react/reference/components/menu), [Popover](https://videojs.org/docs/framework/react/reference/components/popover), [Tooltip](https://videojs.org/docs/framework/react/reference/components/tooltip), and the radio groups for [captions](https://videojs.org/docs/framework/react/reference/components/captions-radio-group), [quality](https://videojs.org/docs/framework/react/reference/components/quality-radio-group), [playback rate](https://videojs.org/docs/framework/react/reference/components/playback-rate-radio-group), and [audio tracks](https://videojs.org/docs/framework/react/reference/components/audio-track-radio-group) |
| Media details and loading | [Title](https://videojs.org/docs/framework/react/reference/components/title), [Poster](https://videojs.org/docs/framework/react/reference/components/poster), [Buffering indicator](https://videojs.org/docs/framework/react/reference/components/buffering-indicator) |
| Feedback after an action | [Seek indicator](https://videojs.org/docs/framework/react/reference/components/seek-indicator), [Volume indicator](https://videojs.org/docs/framework/react/reference/components/volume-indicator), [Status indicator](https://videojs.org/docs/framework/react/reference/components/status-indicator) |
| Errors | [Error dialog](https://videojs.org/docs/framework/react/reference/components/error-dialog) |
| Keyboard, gestures, and announcements | [Hotkey](https://videojs.org/docs/framework/react/reference/components/hotkey), [Gesture](https://videojs.org/docs/framework/react/reference/components/gesture), [Status announcer](https://videojs.org/docs/framework/react/reference/components/status-announcer) |
| Icons | [Icons](https://videojs.org/docs/framework/react/reference/components/icons) |

Some of these are easy to leave out by accident because they have little or no visible UI. Include the error dialog, keyboard shortcuts, and status announcer unless you replace them with your own.

To build a control Video.js doesn’t provide, follow [Build your own UI component](https://videojs.org/docs/framework/react/guides/build-your-own-component).

## Style it your way

The components bring no visual styles. You add the icons, text, and CSS. They describe their state with `data-*` attributes, such as `data-paused` on the play button, so you can style them with plain CSS, Tailwind, or anything else. [UI components](https://videojs.org/docs/framework/react/guides/ui-components) explains how. [Automatically show and hide controls](https://videojs.org/docs/framework/react/guides/controls) covers the controls’ visibility states.

Lay out controls in source order that matches their visual order, so keyboard focus follows the layout.

A player’s controls sit over a moving picture, in a frame whose size you don’t control, so a few design questions come up in every skin:

- **Surfaces over video.** Borders, shadows, and translucent backgrounds look different over every frame. [Shapes and surfaces](https://videojs.org/blog/videojs-v10-skin-design#shapes-and-surfaces) and [Letting the content influence the color](https://videojs.org/blog/videojs-v10-skin-design#letting-the-content-influence-the-color) cover how our skins handle them.
- **Readable controls.** Seek tracks need a visible target, icons need optical alignment, and time displays need fixed-width digits. See [Controls and typography](https://videojs.org/blog/videojs-v10-skin-design#controls-and-typography).
- **Motion.** Menus should open from the control that opened them and respond when they’re closed mid-transition. See [Motion and transitions](https://videojs.org/blog/videojs-v10-skin-design#motion-and-transitions).
- **Small players.** Captions, settings, picture-in-picture, and Cast all compete for space at narrow widths. See [Fitting the available space](https://videojs.org/blog/videojs-v10-skin-design#fitting-the-available-space).
- **Custom colors.** An accent color someone else picks still needs contrast against its text and the video. See [Customization and contrast](https://videojs.org/blog/videojs-v10-skin-design#customization-and-contrast).

[Design inspiration behind the Video.js v10 skins](https://videojs.org/blog/videojs-v10-skin-design) covers the thinking behind our own skins in full.

## Support the public CSS properties

If the skin is only for your own site, you can skip this.

If you want to offer it as a Video.js skin, support the same CSS custom properties as the packaged skins, such as `--media-accent-color` and `--media-border-radius`. People can then restyle your skin the same way they restyle ours. [Set the skin’s styles](https://videojs.org/docs/framework/react/guides/customize-skins#set-the-skins-styles) lists them. We’re still working out how to accept community skins into Video.js.

## Test it

Check the finished skin with real media, at the narrowest player size you support:

- Play, pause, seek, change volume, open every menu, and enter fullscreen.
- Use only a keyboard, then only touch. Focus stays visible and follows the layout, and controls hide when idle.
- Check loading, errors, ended playback, captions, and controls the browser doesn’t support.
- Check text and icon contrast over bright and dark footage, with forced colors, and with reduced motion.

[Accessibility](https://videojs.org/docs/framework/react/guides/accessibility) covers these checks in more detail.

## Related pages

### Guides

- [Skins](https://videojs.org/docs/framework/react/guides/skins): Packaged player designs that include both UI components and their styles.
- [Customize skins](https://videojs.org/docs/framework/react/guides/customize-skins): Style a packaged Video.js skin or add its source to change controls, layout, styles, and interactions
- [Build your own UI component](https://videojs.org/docs/framework/react/guides/build-your-own-component): Create custom player controls that read state, dispatch actions, and stay accessible.

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
