Installation
Install Video.js and build your first player with streaming support and accessible controls
Video.js is a React video player component library — composable primitives, hooks, and TypeScript types for building accessible, customizable players with minimal bundle size.
Video.js is an HTML video player built on custom elements — lightweight, framework-free components for building accessible, customizable players with minimal bundle size.
The code examples in this guide can be tailored to your use case. Run npx @videojs/cli docs how-to/installation interactively, or pass all flags:
npx @videojs/cli docs how-to/installation \
--framework <html|react> \
--preset <video|audio|background-video> \
--skin <default|minimal> \
--media <html5-video|html5-audio|hls|background-video> \
--source-url <url> \
--install-method <cdn|npm|pnpm|yarn|bun>Answer the questions below to get started quickly with your first embed code.
Choose your JS framework
Video.js aims to provide idiomatic development experiences in your favorite JS and CSS frameworks. More to come.
Choose your use case
The default presets work well for general website playback. More pre-built players to come.
Choose skin
Choose how your player looks.
Want full control over the skin? You can eject it copy the source into your project and customize it freely.
Choose your media source type
Video.js supports a wide range of file types and hosting services. It’s easy to switch between them.
Select your source
Or upload your media for free to Mux
Install Video.js
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/video.js"></script>npm install @videojs/htmlpnpm add @videojs/htmlyarn add @videojs/htmlbun add @videojs/htmlnpm install @videojs/reactpnpm add @videojs/reactyarn add @videojs/reactbun add @videojs/reactUse your player
<!--
The PlayerProvider passes state between the UI components
and Media, and makes fully custom UIs possible.
It does not have layout by default (display:contents)
-->
<video-player>
<!--
Skins contain the entire player UI and are easily swappable.
They can each be "ejected" for full control and customization
of UI components.
-->
<video-skin>
<!--
Media are players without UIs, handling networking
and display of the media. They are easily swappable
to handle different sources.
-->
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" playsinline></video>
</video-skin>
</video-player>Create your player
Add it to your components folder in a new file.
'use client';
import '@videojs/react/video/skin.css';
import { createPlayer, videoFeatures } from '@videojs/react';
import { VideoSkin, Video } from '@videojs/react/video';
const Player = createPlayer({ features: videoFeatures });
interface MyPlayerProps {
src: string;
}
export const MyPlayer = ({ src }: MyPlayerProps) => {
return (
<Player.Provider>
<VideoSkin>
<Video src={src} playsInline />
</VideoSkin>
</Player.Provider>
);
};Use your player
import { MyPlayer } from '../components/player';
export const HomePage = () => {
return (
<div>
<h1>Welcome to My App</h1>
<MyPlayer src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" />
</div>
);
};Ejecting a skin
Skins ship pre-built. To customize beyond CSS variables, eject — copy the skin source into your project and own it from there. Pick whichever built-in skin is closest to your goal as a starting point.
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/video-ui.js"></script>
<link rel="stylesheet" href="./player.css">
<video-player>
<media-container class="media-default-skin media-default-skin--video">
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" playsinline></video>
<media-poster>
<img src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.webp" />
</media-poster>
<media-buffering-indicator class="media-buffering-indicator">
<div class="media-surface">
<media-icon name="spinner" class="media-icon"></media-icon>
</div>
</media-buffering-indicator>
<media-error-dialog class="media-error">
<div class="media-error__dialog media-surface">
<div class="media-error__content">
<media-alert-dialog-title class="media-error__title">Something went wrong.</media-alert-dialog-title>
<media-alert-dialog-description class="media-error__description"></media-alert-dialog-description>
</div>
<div class="media-error__actions">
<media-alert-dialog-close class="media-button media-button--primary">OK</media-alert-dialog-close>
</div>
</div>
</media-error-dialog>
<media-controls class="media-surface media-controls">
<media-tooltip-group>
<div class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
<media-icon name="restart" class="media-icon media-icon--restart"></media-icon>
<media-icon name="play" class="media-icon media-icon--play"></media-icon>
<media-icon name="pause" class="media-icon media-icon--pause"></media-icon>
</media-play-button>
<media-tooltip id="play-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-seek-button commandfor="seek-backward-tooltip" seconds="-10" class="media-button media-button--subtle media-button--icon media-button--seek">
<span class="media-icon__container">
<media-icon name="seek" class="media-icon media-icon--flipped"></media-icon>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-backward-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-seek-button commandfor="seek-forward-tooltip" seconds="10" class="media-button media-button--subtle media-button--icon media-button--seek">
<span class="media-icon__container">
<media-icon name="seek" class="media-icon"></media-icon>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-forward-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
</div>
<div class="media-time-controls">
<media-time type="current" class="media-time"></media-time>
<media-time-slider class="media-slider">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
<div class="media-surface media-preview media-slider__preview">
<media-slider-thumbnail class="media-preview__thumbnail"></media-slider-thumbnail>
<media-slider-value type="pointer" class="media-time media-preview__time"></media-slider-value>
<media-icon name="spinner" class="media-preview__spinner media-icon"></media-icon>
</div>
</media-time-slider>
<media-time type="duration" class="media-time"></media-time>
</div>
<div class="media-button-group">
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--subtle media-button--icon media-button--playback-rate"></media-playback-rate-button>
<media-tooltip id="playback-rate-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--subtle media-button--icon media-button--mute">
<media-icon name="volume-off" class="media-icon media-icon--volume-off"></media-icon>
<media-icon name="volume-low" class="media-icon media-icon--volume-low"></media-icon>
<media-icon name="volume-high" class="media-icon media-icon--volume-high"></media-icon>
</media-mute-button>
<media-popover id="video-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popover media-popover--volume">
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
</media-volume-slider>
</media-popover>
<media-captions-button commandfor="captions-tooltip" class="media-button media-button--subtle media-button--icon media-button--captions">
<media-icon name="captions-off" class="media-icon media-icon--captions-off"></media-icon>
<media-icon name="captions-on" class="media-icon media-icon--captions-on"></media-icon>
</media-captions-button>
<media-tooltip id="captions-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-cast-button commandfor="cast-tooltip" class="media-button media-button--subtle media-button--icon media-button--cast">
<media-icon name="cast-enter" class="media-icon media-icon--cast-enter"></media-icon>
<media-icon name="cast-exit" class="media-icon media-icon--cast-exit"></media-icon>
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--subtle media-button--icon media-button--pip">
<media-icon name="pip-enter" class="media-icon media-icon--pip-enter"></media-icon>
<media-icon name="pip-exit" class="media-icon media-icon--pip-exit"></media-icon>
</media-pip-button>
<media-tooltip id="pip-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-fullscreen-button commandfor="fullscreen-tooltip" class="media-button media-button--subtle media-button--icon media-button--fullscreen">
<media-icon name="fullscreen-enter" class="media-icon media-icon--fullscreen-enter"></media-icon>
<media-icon name="fullscreen-exit" class="media-icon media-icon--fullscreen-exit"></media-icon>
</media-fullscreen-button>
<media-tooltip id="fullscreen-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
</div>
</media-tooltip-group>
</media-controls>
<div class="media-overlay"></div>
<!-- Hotkeys -->
<media-hotkey keys="Space" action="togglePaused"></media-hotkey>
<media-hotkey keys="k" action="togglePaused"></media-hotkey>
<media-hotkey keys="m" action="toggleMuted"></media-hotkey>
<media-hotkey keys="f" action="toggleFullscreen"></media-hotkey>
<media-hotkey keys="c" action="toggleSubtitles"></media-hotkey>
<media-hotkey keys="i" action="togglePictureInPicture"></media-hotkey>
<media-hotkey keys="ArrowRight" action="seekStep" value="5"></media-hotkey>
<media-hotkey keys="ArrowLeft" action="seekStep" value="-5"></media-hotkey>
<media-hotkey keys="l" action="seekStep" value="10"></media-hotkey>
<media-hotkey keys="j" action="seekStep" value="-10"></media-hotkey>
<media-hotkey keys="ArrowUp" action="volumeStep" value="0.05"></media-hotkey>
<media-hotkey keys="ArrowDown" action="volumeStep" value="-0.05"></media-hotkey>
<media-hotkey keys="0-9" action="seekToPercent"></media-hotkey>
<media-hotkey keys="Home" action="seekToPercent" value="0"></media-hotkey>
<media-hotkey keys="End" action="seekToPercent" value="100"></media-hotkey>
<media-hotkey keys=">" action="speedUp"></media-hotkey>
<media-hotkey keys="<" action="speedDown"></media-hotkey>
<!-- Gestures -->
<media-gesture type="tap" action="togglePaused" pointer="mouse" region="center"></media-gesture>
<media-gesture type="tap" action="toggleControls" pointer="touch"></media-gesture>
<media-gesture type="doubletap" action="seekStep" value="-10" region="left"></media-gesture>
<media-gesture type="doubletap" action="toggleFullscreen" region="center"></media-gesture>
<media-gesture type="doubletap" action="seekStep" value="10" region="right"></media-gesture>
<!-- Input Feedback -->
<media-status-announcer></media-status-announcer>
<div class="media-input-feedback">
<media-volume-indicator hidden class="media-surface media-input-feedback-island media-input-feedback-island--volume">
<media-volume-indicator-fill class="media-input-feedback-island__content">
<media-icon name="volume-high" class="media-icon media-icon--volume-high"></media-icon>
<media-icon name="volume-low" class="media-icon media-icon--volume-low"></media-icon>
<media-icon name="volume-off" class="media-icon media-icon--volume-off"></media-icon>
<media-volume-indicator-value class="media-input-feedback-island__value"></media-volume-indicator-value>
</media-volume-indicator-fill>
</media-volume-indicator>
<media-status-indicator
hidden
actions="toggleSubtitles toggleFullscreen togglePictureInPicture"
class="media-surface media-input-feedback-island media-input-feedback-island--status"
>
<div class="media-input-feedback-island__content">
<media-icon name="captions-on" class="media-icon media-icon--captions-on"></media-icon>
<media-icon name="captions-off" class="media-icon media-icon--captions-off"></media-icon>
<media-icon name="fullscreen-enter" class="media-icon media-icon--fullscreen-enter"></media-icon>
<media-icon name="fullscreen-exit" class="media-icon media-icon--fullscreen-exit"></media-icon>
<media-icon name="pip-enter" class="media-icon media-icon--pip-enter"></media-icon>
<media-icon name="pip-exit" class="media-icon media-icon--pip-exit"></media-icon>
<media-status-indicator-value class="media-input-feedback-island__value"></media-status-indicator-value>
</div>
</media-status-indicator>
<media-seek-indicator hidden class="media-input-feedback-bubble">
<media-icon name="chevron" class="media-icon media-icon--seek"></media-icon>
<media-seek-indicator-value class="media-time"></media-seek-indicator-value>
</media-seek-indicator>
<media-status-indicator hidden actions="togglePaused" class="media-input-feedback-bubble">
<media-icon name="play" class="media-icon media-icon--play"></media-icon>
<media-icon name="pause" class="media-icon media-icon--pause"></media-icon>
</media-status-indicator>
</div>
</media-container>
</video-player>/* -------------------------------------------------------------------------- */
/* Global styles for the host document, outside of the Shadow DOM */
/* -------------------------------------------------------------------------- */
video-player,
live-video-player {
display: contents;
}
/*
Required to override any default video and image styles (such as
Tailwind's CSS reset) and ensure they fill the container as expected.
*/
video-player video,
video-player [slot="poster"],
live-video-player video,
live-video-player [slot="poster"] {
display: block;
width: 100%;
height: 100%;
}
video-player video::-webkit-media-text-track-container,
live-video-player video::-webkit-media-text-track-container {
z-index: 1;
font-family: inherit;
scale: 0.98;
translate: 0 var(--media-caption-track-y, 0);
transition: translate var(--media-caption-track-duration, 0) ease-out;
transition-delay: var(--media-caption-track-delay, 0);
}
/* -------------------------------------------------------------------------- */
/* Shared styles for all HTML skins */
/* -------------------------------------------------------------------------- */
media-tooltip-group {
display: contents;
}
:host {
/* `display:grid` fixes a weird issue with Safari when setting aspect-ratio */
display: grid;
width: 100%;
}
/* Hide volume popover when volume control is unsupported (e.g., iOS Safari). */
.media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}
/* ==========================================================================
Reset
========================================================================== */
.media-default-skin *,
.media-default-skin *::before,
.media-default-skin *::after {
box-sizing: border-box;
}
.media-default-skin img,
.media-default-skin video,
.media-default-skin svg {
display: block;
max-width: 100%;
}
.media-default-skin button {
font: inherit;
}
.media-default-skin [hidden][hidden] {
/* Keep authored templates hidden even when component classes set display. */
display: none;
}
@media (prefers-reduced-motion: no-preference) {
.media-default-skin {
interpolate-size: allow-keywords;
}
}
/* ==========================================================================
Root Container
========================================================================== */
.media-default-skin {
--media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
--media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
--media-icon-size: 18px;
position: relative;
display: block;
width: 100%;
height: 100%;
container: media-root / inline-size;
font-family:
Inter Variable,
Inter,
ui-sans-serif,
system-ui,
sans-serif;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
line-height: 1.5;
letter-spacing: normal;
outline: 2px solid transparent;
outline-offset: -4px;
border-radius: var(--media-border-radius, 2rem);
isolation: isolate;
transition-timing-function: ease-out;
transition-duration: 100ms;
transition-property: outline-offset, outline-color;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
& > * {
font-size: 0.75rem; /* 12px at 100% font size */
@container media-root (width > 48rem) {
font-size: 0.875rem; /* 14px at 100% font size */
}
}
}
/* ==========================================================================
Surface (shared glass effect for tooltips, popovers, controls)
========================================================================== */
.media-default-skin .media-surface {
background-color: var(--media-surface-background-color);
box-shadow:
0 0 0 1px var(--media-surface-outer-border-color),
0 1px 3px 0 var(--media-surface-shadow-color),
0 1px 2px -1px var(--media-surface-shadow-color);
backdrop-filter: var(--media-surface-backdrop-filter);
/* Inner border ring */
&::after {
position: absolute;
inset: 0;
z-index: 10;
pointer-events: none;
content: "";
border-radius: inherit;
box-shadow: inset 0 0 0 1px var(--media-surface-inner-border-color);
}
}
/* ==========================================================================
Media Element
========================================================================== */
.media-default-skin ::slotted(video),
.media-default-skin video {
display: block;
width: 100%;
height: 100%;
object-fit: var(--media-object-fit, contain);
object-position: var(--media-object-position, center);
}
.media-default-skin ::slotted(video) {
border-radius: var(--media-video-border-radius);
}
.media-default-skin video {
border-radius: inherit;
}
.media-default-skin:fullscreen ::slotted(video),
.media-default-skin:fullscreen video {
object-fit: contain;
}
/* ==========================================================================
Overlay / Scrim
========================================================================== */
.media-default-skin .media-overlay {
position: absolute;
inset: 0;
pointer-events: none;
background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
border-radius: inherit;
opacity: 0;
backdrop-filter: blur(0) saturate(1);
transition-timing-function: ease-out;
transition-duration: var(--media-controls-transition-duration);
transition-property: opacity, backdrop-filter;
}
.media-default-skin .media-error ~ .media-overlay {
transition-delay: var(--media-error-dialog-transition-delay);
transition-duration: var(--media-error-dialog-transition-duration);
}
.media-default-skin .media-controls[data-visible] ~ .media-overlay,
.media-default-skin .media-error[data-open] ~ .media-overlay {
opacity: 1;
}
.media-default-skin .media-error[data-open] ~ .media-overlay {
backdrop-filter: blur(16px) saturate(1.5);
}
/* ==========================================================================
Buffering Indicator
========================================================================== */
.media-default-skin .media-buffering-indicator {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
color: oklch(1 0 0);
pointer-events: none;
&:not([data-visible]) {
--media-spinner-animation: none;
}
&[data-visible] {
display: flex;
}
.media-surface {
padding: 0.25rem;
border-radius: 100%;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-default-skin .media-error {
outline: none;
}
.media-default-skin .media-error:not([data-open]) {
display: none;
}
.media-default-skin .media-error__title {
font-weight: 600;
line-height: 1.25;
}
.media-default-skin .media-error__description {
overflow-wrap: anywhere;
opacity: 0.7;
}
.media-default-skin .media-error__actions {
display: flex;
gap: 0.5rem;
& > * {
flex: 1;
}
}
.media-default-skin .media-error[data-open] ~ .media-controls * {
visibility: hidden;
}
/* ==========================================================================
Controls
========================================================================== */
.media-default-skin .media-controls {
display: flex;
column-gap: 0.075rem;
align-items: center;
padding: 0.375rem;
container: media-controls / inline-size;
text-shadow: 0 1px 0 var(--media-current-shadow-color);
border-radius: 1.5rem;
}
/* ==========================================================================
Time Display
========================================================================== */
.media-default-skin .media-time-controls {
display: flex;
flex: 1;
gap: 0.75rem;
align-items: center;
padding-inline: 0.5rem;
container: media-time-controls / inline-size;
}
.media-default-skin .media-time {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Buttons
========================================================================== */
/* Base button */
.media-default-skin .media-button {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
min-height: 0;
padding: 0.5rem 1rem;
text-align: center;
touch-action: manipulation;
cursor: pointer;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
border: none;
border-radius: calc(infinity * 1px);
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: background-color, outline-offset, scale;
/* Fix weird jumping when clicking on the buttons in Safari. */
will-change: scale;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
&:active {
scale: 0.98;
}
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
&[data-availability="unavailable"],
&[data-availability="unsupported"] {
display: none;
}
}
/* Primary button variant */
.media-default-skin .media-button--primary {
font-weight: 500;
color: oklch(0 0 0);
text-shadow: none;
background: oklch(1 0 0);
}
/* Subtle button variant */
.media-default-skin .media-button--subtle {
color: inherit;
text-shadow: inherit;
background: transparent;
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
text-decoration: none;
background-color: oklch(from currentColor l c h / 0.1);
}
}
/* Icon button variant */
.media-default-skin .media-button--icon {
display: grid;
width: 2.25rem;
aspect-ratio: 1;
padding: 0;
&:active {
scale: 0.9;
}
& .media-icon {
grid-area: 1 / 1;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
}
}
/* Seek button */
.media-default-skin .media-button--seek {
& .media-icon__label {
position: absolute;
right: -1px;
bottom: -3px;
font-size: 10px;
font-weight: 480;
font-variant-numeric: tabular-nums;
}
&:has(.media-icon--flipped) .media-icon__label {
right: unset;
left: -1px;
}
}
/* Playback rate button */
.media-default-skin .media-button--playback-rate {
padding: 0;
&::after {
width: 4ch;
font-variant-numeric: tabular-nums;
content: attr(data-rate) "\00D7";
}
}
/* Live button — wide pill button with a status dot (gray → red at the live
edge) rendered via ::before, and "LIVE" text rendered as the button's own
text content. */
.media-default-skin .media-button--live {
display: inline-flex;
gap: 0.4rem;
align-items: center;
width: auto;
aspect-ratio: auto;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
text-transform: uppercase;
letter-spacing: 0.05em;
&::before {
display: inline-block;
flex-shrink: 0;
width: 0.5rem;
height: 0.5rem;
content: "";
background-color: oklch(from currentColor l c h / 0.4);
border-radius: 50%;
transition: background-color 150ms ease-out;
}
&[data-live-edge]::before {
background-color: oklch(0.65 0.22 27);
}
}
/* ==========================================================================
Button Groups
========================================================================== */
.media-default-skin .media-button-group {
display: flex;
gap: 0.075rem;
align-items: center;
@container media-root (width > 42rem) {
gap: 0.125rem;
}
}
/* ==========================================================================
Icons
========================================================================== */
.media-default-skin .media-icon__container {
position: relative;
}
.media-default-skin .media-icon {
flex-shrink: 0;
width: var(--media-icon-size);
height: var(--media-icon-size);
}
.media-default-skin .media-icon--flipped {
scale: -1 1;
}
/* ==========================================================================
Poster Image
========================================================================== */
.media-default-skin media-poster,
.media-default-skin > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
transition: opacity 0.25s;
}
.media-default-skin media-poster:not([data-visible]),
.media-default-skin > img:not([data-visible]) {
opacity: 0;
}
.media-default-skin media-poster ::slotted(img),
.media-default-skin media-poster img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: var(--media-object-fit, contain);
object-position: var(--media-object-position, center);
border-radius: var(--media-video-border-radius);
}
.media-default-skin > img {
object-fit: var(--media-object-fit, contain);
object-position: var(--media-object-position, center);
border-radius: inherit;
}
.media-default-skin:fullscreen media-poster ::slotted(img),
.media-default-skin:fullscreen media-poster img,
.media-default-skin:fullscreen > img {
object-fit: contain;
}
/* ==========================================================================
Media preview
========================================================================== */
.media-default-skin .media-preview {
pointer-events: none;
background-color: oklch(0 0 0 / 0.9);
border-radius: 0.75rem;
& .media-preview__thumbnail {
position: relative;
display: block;
overflow: clip;
border-radius: inherit;
&::after {
position: absolute;
inset: 0;
content: "";
background-image: linear-gradient(to top, oklch(0 0 0 / 0.8), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
border-radius: inherit;
}
}
& .media-preview__time {
position: absolute;
inset-inline: 0;
bottom: 0.5rem;
text-align: center;
}
& .media-overlay {
opacity: 1;
}
& .media-preview__spinner {
position: absolute;
top: 50%;
left: 50%;
opacity: 0;
translate: -50% -50%;
}
& .media-preview__thumbnail,
& .media-preview__spinner {
transition: opacity 150ms ease-out;
}
&:not(:has(.media-preview__thumbnail[data-loading])) {
& .media-preview__spinner {
--media-spinner-animation: none;
}
}
&:has(.media-preview__thumbnail[data-loading]) {
& .media-preview__thumbnail {
opacity: 0;
}
& .media-preview__spinner {
opacity: 1;
}
}
}
/* ==========================================================================
Slider
========================================================================== */
.media-default-skin .media-slider {
position: relative;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
border-radius: calc(infinity * 1px);
&[data-orientation="horizontal"] {
width: 100%;
min-width: 5rem;
height: 2rem;
}
&[data-orientation="vertical"] {
width: 2rem;
height: 5rem;
}
}
/* Track */
.media-default-skin .media-slider__track {
position: relative;
overflow: hidden;
user-select: none;
border-radius: inherit;
isolation: isolate;
&[data-orientation="horizontal"] {
width: 100%;
height: 0.25rem;
}
&[data-orientation="vertical"] {
width: 0.25rem;
height: 100%;
}
}
/* Thumb */
.media-default-skin .media-slider__thumb {
position: absolute;
z-index: 10;
width: 0.625rem;
height: 0.625rem;
user-select: none;
outline: 4px solid transparent;
outline-offset: -4px;
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
0 1px 3px 0 oklch(0 0 0 / 0.15),
0 1px 2px -1px oklch(0 0 0 / 0.15);
opacity: 0;
translate: -50% -50%;
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: opacity, height, width, outline-offset;
&[data-orientation="horizontal"] {
top: 50%;
left: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
top: calc(100% - var(--media-slider-fill));
left: 50%;
}
&:hover,
&:focus {
outline-color: oklch(from currentColor l c h / 0.25);
outline-offset: 0;
}
&::after {
position: absolute;
inset: -4px;
content: "";
border-radius: inherit;
box-shadow: 0 0 0 2px oklch(1 0 0);
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: opacity, scale;
}
&:not(:focus-visible)::after {
opacity: 0;
scale: 0.5;
}
}
.media-default-skin .media-slider:active .media-slider__thumb,
.media-default-skin .media-slider__thumb--persistent {
width: 0.75rem;
height: 0.75rem;
}
.media-default-skin .media-slider:hover .media-slider__thumb,
.media-default-skin .media-slider__thumb:focus-visible,
.media-default-skin .media-slider__thumb--persistent {
opacity: 1;
}
/* Shared track fills */
.media-default-skin .media-slider__buffer,
.media-default-skin .media-slider__fill {
position: absolute;
pointer-events: none;
border-radius: inherit;
}
.media-default-skin .media-slider__buffer[data-orientation="horizontal"],
.media-default-skin .media-slider__fill[data-orientation="horizontal"] {
inset-block: 0;
left: 0;
}
.media-default-skin .media-slider__buffer[data-orientation="vertical"],
.media-default-skin .media-slider__fill[data-orientation="vertical"] {
inset-inline: 0;
bottom: 0;
}
/* Buffer */
.media-default-skin .media-slider__buffer {
background-color: oklch(from currentColor l c h / 0.2);
transition-timing-function: ease-out;
transition-duration: 0.25s;
&[data-orientation="horizontal"] {
width: var(--media-slider-buffer);
transition-property: width;
}
&[data-orientation="vertical"] {
height: var(--media-slider-buffer);
transition-property: height;
}
}
/* Fill */
.media-default-skin .media-slider__fill {
background-color: currentColor;
&[data-orientation="horizontal"] {
width: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
height: var(--media-slider-fill);
}
}
/* Dragging — thumb and fill follow the pointer position */
.media-default-skin .media-slider[data-dragging] .media-slider__thumb[data-orientation="horizontal"] {
left: var(--media-slider-pointer);
}
.media-default-skin .media-slider[data-dragging] .media-slider__thumb[data-orientation="vertical"] {
top: calc(100% - var(--media-slider-pointer));
}
.media-default-skin .media-slider[data-dragging] .media-slider__fill[data-orientation="horizontal"] {
width: var(--media-slider-pointer);
}
.media-default-skin .media-slider[data-dragging] .media-slider__fill[data-orientation="vertical"] {
height: var(--media-slider-pointer);
}
/* ==========================================================================
Popups & Tooltips
========================================================================== */
.media-default-skin .media-popover,
.media-default-skin .media-tooltip {
margin: 0;
overflow: visible;
color: inherit;
border: 0;
transition-timing-function: var(--media-popup-transition-timing-function);
transition-duration: var(--media-popup-transition-duration);
transition-property: scale, opacity, filter;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
filter: blur(8px);
scale: 0.5;
}
&[data-instant] {
transition-duration: 0ms;
}
&[data-side="top"] {
transform-origin: bottom;
}
&[data-side="bottom"] {
transform-origin: top;
}
&[data-side="left"] {
transform-origin: right;
}
&[data-side="right"] {
transform-origin: left;
}
/* Safe area between trigger and popup */
&::before {
position: absolute;
pointer-events: inherit;
content: "";
}
&[data-side="top"]::before,
&[data-side="bottom"]::before {
inset-inline: 0;
width: 100%;
}
&[data-side="top"]::before {
top: 100%;
}
&[data-side="bottom"]::before {
bottom: 100%;
}
&[data-side="left"]::before,
&[data-side="right"]::before {
inset-block: 0;
height: 100%;
}
&[data-side="left"]::before {
left: 100%;
}
&[data-side="right"]::before {
right: 100%;
}
}
.media-default-skin .media-popover {
&[data-side="top"]::before,
&[data-side="bottom"]::before {
height: var(--media-popover-side-offset);
}
&[data-side="left"]::before,
&[data-side="right"]::before {
width: var(--media-popover-side-offset);
}
}
.media-default-skin .media-popover--volume {
padding: 0.75rem 0;
border-radius: calc(infinity * 1px);
&:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}
}
.media-default-skin .media-tooltip {
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
white-space: nowrap;
border-radius: calc(infinity * 1px);
&[data-side="top"]::before,
&[data-side="bottom"]::before {
height: var(--media-tooltip-side-offset);
}
&[data-side="left"]::before,
&[data-side="right"]::before {
width: var(--media-tooltip-side-offset);
}
}
/* ==========================================================================
Native Caption Track
========================================================================== */
.media-default-skin {
--media-caption-track-duration: var(--media-controls-transition-duration);
--media-caption-track-delay: 25ms;
--media-caption-track-y: -0.5rem;
&:has(.media-controls[data-visible]) {
--media-caption-track-y: -5.5rem;
}
@container media-root (width > 42rem) {
&:has(.media-controls[data-visible]) > * {
--media-caption-track-y: -3.5rem;
}
}
}
.media-default-skin video::-webkit-media-text-track-container {
z-index: 1;
font-family: inherit;
scale: 0.98;
translate: 0 var(--media-caption-track-y);
transition: translate var(--media-caption-track-duration) ease-out;
transition-delay: var(--media-caption-track-delay);
}
/* ==========================================================================
Input Feedback
========================================================================== */
.media-default-skin .media-input-feedback {
position: absolute;
inset-inline: 0;
top: 0;
bottom: 3.5rem; /* Shift up a little in smaller containers */
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
justify-items: center;
color: var(--media-color-primary, oklch(1 0 0));
pointer-events: none;
@container media-root (width > 24rem) {
bottom: 0;
}
}
/* --- Feedback islands ------------------------------------------------------- */
.media-default-skin .media-input-feedback-island {
--media-surface-background-color: oklch(0 0 0 / 0.25);
position: absolute;
top: 0.75rem;
font-weight: 500;
color: inherit;
pointer-events: none;
border-radius: calc(Infinity * 1px);
transform-origin: top center;
transition-timing-function: ease-out;
transition-duration: 100ms;
.media-input-feedback-island__content {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.25rem 0.625rem;
/* Increase contrast of the content */
* {
mix-blend-mode: difference;
}
}
.media-icon {
display: none;
flex-shrink: 0;
}
.media-input-feedback-island__value {
margin-left: auto;
}
@media (pointer: coarse) {
transition-property: scale, translate, opacity;
will-change: scale, translate, opacity;
}
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
transition-property: scale, translate, filter, opacity;
will-change: scale, translate, filter, opacity;
}
@media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
--media-surface-background-color: oklch(0 0 0);
}
/* Default hidden state */
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transition-timing-function: ease-in;
transition-duration: 250ms;
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
filter: blur(8px);
scale: 0.9;
}
@media (prefers-reduced-motion: no-preference) {
&[data-ending-style] {
translate: 0 -25%;
}
}
}
}
.media-default-skin .media-input-feedback-island--volume {
width: min(80%, 12rem);
.media-input-feedback-island__content {
--media-progress-fill: var(--media-volume-fill);
background-image: linear-gradient(
to right,
currentColor 0%,
currentColor var(--media-progress-fill),
transparent var(--media-progress-fill),
transparent 100%
);
border-radius: inherit;
transition: --media-progress-fill 200ms linear;
}
}
.media-default-skin .media-input-feedback-island--volume[data-level="high"] .media-icon--volume-high,
.media-default-skin .media-input-feedback-island--volume[data-level="low"] .media-icon--volume-low,
.media-default-skin .media-input-feedback-island--volume[data-level="off"] .media-icon--volume-off {
display: block;
}
.media-default-skin .media-input-feedback-island--status[data-status="captions-on"] .media-icon--captions-on,
.media-default-skin .media-input-feedback-island--status[data-status="captions-off"] .media-icon--captions-off,
.media-default-skin .media-input-feedback-island--status[data-status="fullscreen"] .media-icon--fullscreen-enter,
.media-default-skin .media-input-feedback-island--status[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
.media-default-skin .media-input-feedback-island--status[data-status="pip"] .media-icon--pip-enter,
.media-default-skin .media-input-feedback-island--status[data-status="exit-pip"] .media-icon--pip-exit {
display: block;
}
/* --- Boundary shake ------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
.media-default-skin .media-input-feedback-island--volume[data-min],
.media-default-skin .media-input-feedback-island--volume[data-max] {
animation: media-shake 300ms ease-in-out;
}
}
/* --- Bubble ---------------------------------------------------------------- */
.media-default-skin .media-input-feedback-bubble {
display: flex;
flex-direction: column;
grid-row: 1;
grid-column: 2; /* default to center for status bubbles and undirected seeks */
align-items: center;
justify-content: center;
padding: 1rem;
transition: opacity 250ms ease-out;
@container media-root (width > 24rem) {
padding: 2rem;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transition-timing-function: ease-in;
transition-duration: 200ms;
}
}
/* Direction placement — seek bubbles move to the side implied by their direction. */
.media-default-skin .media-input-feedback-bubble[data-direction="backward"] {
grid-column: 1;
justify-self: left;
}
.media-default-skin .media-input-feedback-bubble:not([data-direction]) {
grid-column: 2;
transition-timing-function:
ease-out, linear(0, 0.12 1.5%, 1.35 9.7%, 2.2 13.9%, 3 19.9%, 2.7 21.8%, 0.62 37.5%, 0.96 50.9%, 1);
transition-duration: 600ms;
transition-property: opacity, scale;
@media (prefers-reduced-motion: reduce) {
transition: opacity 100ms ease-out;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
scale: 0.8;
transition-timing-function: ease-in;
transition-duration: 200ms;
}
}
.media-default-skin .media-input-feedback-bubble[data-direction="forward"] {
grid-column: 3;
justify-self: right;
}
/* --- Bubble icons ---------------------------------------------------------- */
.media-default-skin .media-input-feedback-bubble .media-icon {
display: none;
width: 36px;
height: 36px;
}
/* seek: seek icon, flipped for backward */
.media-default-skin .media-input-feedback-bubble[data-direction] .media-icon--seek {
display: block;
}
.media-default-skin .media-input-feedback-bubble[data-direction="backward"] .media-icon--seek {
transform: scaleX(-1);
}
@media (prefers-reduced-motion: no-preference) {
.media-default-skin
.media-input-feedback-bubble[data-direction="forward"]:not([data-starting-style])
.media-icon--seek {
animation: media-slide-in-forward 300ms ease-in-out;
}
.media-default-skin
.media-input-feedback-bubble[data-direction="backward"]:not([data-starting-style])
.media-icon--seek {
animation: media-slide-in-backward 300ms ease-in-out;
}
.media-default-skin .media-input-feedback-island--status[data-status]:not([data-starting-style]) .media-icon,
.media-default-skin .media-input-feedback-bubble[data-status]:not([data-starting-style]) .media-icon {
animation: media-pop-in 250ms ease-out;
}
}
.media-default-skin .media-input-feedback-bubble[data-status="pause"] .media-icon--pause,
.media-default-skin .media-input-feedback-bubble[data-status="play"] .media-icon--play {
display: block;
}
/* ==========================================================================
Icon State Visibility for Video Skins
Data-attribute-driven visibility rules for multi-state icon buttons.
Uses :is() with both element selectors (for HTML custom element wrappers)
and class selectors (for React rendered SVG elements).
========================================================================== */
/* --- All icons hidden by default --- */
.media-button--play .media-icon--restart,
.media-button--play .media-icon--play,
.media-button--play .media-icon--pause,
.media-button--mute .media-icon--volume-off,
.media-button--mute .media-icon--volume-low,
.media-button--mute .media-icon--volume-high,
.media-button--fullscreen .media-icon--fullscreen-enter,
.media-button--fullscreen .media-icon--fullscreen-exit,
.media-button--pip .media-icon--pip-enter,
.media-button--pip .media-icon--pip-exit,
.media-button--cast .media-icon--cast-enter,
.media-button--cast .media-icon--cast-exit,
.media-button--captions .media-icon--captions-off,
.media-button--captions .media-icon--captions-on {
display: none;
opacity: 0;
}
/* --- Active icon per state --- */
/* Play: ended → restart */
.media-button--play[data-ended] .media-icon--restart,
/* Play: paused (not ended) → play */
.media-button--play:not([data-ended])[data-paused] .media-icon--play,
/* Play: playing (not paused, not ended) → pause */
.media-button--play:not([data-paused]):not([data-ended]) .media-icon--pause,
/* Mute: muted → volume off */
.media-button--mute[data-muted] .media-icon--volume-off,
/* Mute: volume low (not muted) → volume low */
.media-button--mute:not([data-muted])[data-volume-level="low"] .media-icon--volume-low,
/* Mute: volume high (not muted, not low) → volume high */
.media-button--mute:not([data-muted]):not([data-volume-level="low"]) .media-icon--volume-high,
/* Fullscreen: not fullscreen → enter */
.media-button--fullscreen:not([data-fullscreen]) .media-icon--fullscreen-enter,
/* Fullscreen: fullscreen → exit */
.media-button--fullscreen[data-fullscreen] .media-icon--fullscreen-exit,
/* Picture-in-Picture: not active → enter */
.media-button--pip:not([data-pip]) .media-icon--pip-enter,
/* Picture-in-Picture: active → exit */
.media-button--pip[data-pip] .media-icon--pip-exit,
/* Cast: not connected → enter */
.media-button--cast:not([data-cast-state="connected"]) .media-icon--cast-enter,
/* Cast: connected → exit */
.media-button--cast[data-cast-state="connected"] .media-icon--cast-exit,
/* Captions: not active → captions off */
.media-button--captions:not([data-active]) .media-icon--captions-off,
/* Captions: active → captions on */
.media-button--captions[data-active] .media-icon--captions-on {
display: block;
opacity: 1;
}
/* -------------------------------------------------------------------------- */
/* Global @keyframes for all video skins (CSS & Tailwind) */
/* -------------------------------------------------------------------------- */
@keyframes media-shake {
0%,
100% {
translate: 0 0;
}
20% {
translate: -6px 0;
}
40% {
translate: 4px 0;
}
60% {
translate: -2px 0;
}
80% {
translate: 1px 0;
}
}
@keyframes media-slide-in-forward {
from {
translate: -60% 0;
opacity: 0;
}
}
@keyframes media-slide-in-backward {
from {
translate: 60% 0;
opacity: 0;
}
}
@keyframes media-pop-in {
from {
scale: 0.8;
opacity: 0;
}
}
/* -------------------------------------------------------------------------- */
/* Global @properties for all video skins (CSS & Tailwind) */
/* -------------------------------------------------------------------------- */
@property --media-progress-fill {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
/* ==========================================================================
Root
========================================================================== */
.media-default-skin--video {
--media-spring-timing-function: linear(
0,
0.034 1.5%,
0.763 9.7%,
1.066 13.9%,
1.198 19.9%,
1.184 21.8%,
0.963 37.5%,
0.997 50.9%,
1
);
--media-border-color: oklch(0 0 0 / 0.1);
--media-surface-background-color: oklch(1 0 0 / 0.1);
--media-surface-inner-border-color: oklch(1 0 0 / 0.05);
--media-surface-outer-border-color: oklch(0 0 0 / 0.1);
--media-surface-shadow-color: oklch(0 0 0 / 0.15);
--media-surface-backdrop-filter: blur(16px) saturate(1.5);
--media-video-border-radius: var(--media-border-radius, 2rem);
--media-controls-transition-duration: 100ms;
--media-controls-transition-timing-function: ease-out;
--media-error-dialog-transition-duration: 350ms;
--media-error-dialog-transition-delay: 100ms;
--media-error-dialog-transition-timing-function: var(--media-spring-timing-function);
--media-popup-transition-duration: 100ms;
--media-popup-transition-timing-function: ease-out;
--media-tooltip-side-offset: 0.75rem;
--media-popover-side-offset: 0.5rem;
background: oklch(0 0 0);
@media (prefers-reduced-motion: reduce) {
--media-error-dialog-transition-duration: 50ms;
--media-error-dialog-transition-delay: 0ms;
--media-error-dialog-transition-timing-function: ease-out;
--media-popup-transition-duration: 0ms;
}
@media (prefers-color-scheme: dark) {
--media-border-color: oklch(1 0 0 / 0.15);
}
@media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
--media-surface-background-color: oklch(0 0 0);
--media-surface-inner-border-color: oklch(1 0 0 / 0.25);
--media-surface-outer-border-color: transparent;
}
&:has(.media-controls:not([data-visible])) {
/* Slight delay to hide controls on non-touch devices after interaction */
@media (pointer: fine) {
--media-controls-transition-duration: 300ms;
}
@media (pointer: coarse) {
--media-controls-transition-duration: 150ms;
}
@media (prefers-reduced-motion: reduce) {
--media-controls-transition-duration: 50ms;
}
}
/* Inner border ring */
&::after {
position: absolute;
inset: 0;
z-index: 10;
pointer-events: none;
content: "";
border-radius: inherit;
box-shadow: inset 0 0 0 1px var(--media-border-color);
}
&:fullscreen {
--media-border-radius: 0;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-default-skin--video .media-error {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
}
.media-default-skin--video .media-error__dialog {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 18rem;
padding: 0.75rem;
color: oklch(1 0 0);
text-shadow: 0 1px 0 oklch(0 0 0 / 0.25);
border-radius: 1.75rem;
transition-delay: var(--media-error-dialog-transition-delay);
transition-timing-function: var(--media-error-dialog-transition-timing-function);
transition-duration: var(--media-error-dialog-transition-duration);
transition-property: opacity, scale;
}
.media-default-skin--video .media-error[data-starting-style] .media-error__dialog,
.media-default-skin--video .media-error[data-ending-style] .media-error__dialog {
opacity: 0;
scale: 0.5;
}
.media-default-skin--video .media-error[data-ending-style] .media-error__dialog {
transition-delay: 0ms;
}
.media-default-skin--video .media-error__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0.5rem 0.375rem;
text-shadow: inherit;
}
.media-default-skin--video .media-error__title {
font-size: 1rem;
}
/* ==========================================================================
Controls (hide/show behavior)
========================================================================== */
.media-default-skin--video .media-controls {
position: absolute;
inset-inline: 0.5rem;
bottom: 0.5rem;
z-index: 10;
flex-wrap: wrap;
color: var(--media-color-primary, oklch(1 0 0));
transform-origin: bottom;
transition-timing-function: var(--media-controls-transition-timing-function);
transition-duration: var(--media-controls-transition-duration);
@media (pointer: fine) {
transition-property: scale, filter, opacity;
will-change: scale, filter, opacity;
}
@media (pointer: coarse) {
transition-property: scale, opacity;
will-change: scale, opacity;
}
&:not([data-visible]) {
pointer-events: none;
opacity: 0;
scale: 0.9;
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
filter: blur(8px);
}
@media (prefers-reduced-motion: reduce) {
scale: 1;
}
}
& .media-time-controls {
flex: 0 0 100%;
order: -1;
padding-inline: 0.625rem;
}
& .media-button-group:first-child {
flex: 1;
text-align: left;
}
& .media-button-group:last-child {
flex: 1;
justify-content: end;
}
@container media-root (width > 42rem) {
inset-inline: 0.75rem;
bottom: 0.75rem;
flex-wrap: nowrap;
column-gap: 0.125rem;
padding: 0.25rem;
& .media-time-controls {
flex: 1;
order: unset;
}
& .media-button-group:first-child,
& .media-button-group:last-child {
flex: 0 0 auto;
}
}
}
.media-default-skin--video .media-error[data-open] ~ .media-controls {
display: none;
}
/* Hide cursor when controls are hidden */
.media-default-skin--video:has(.media-controls:not([data-visible])) {
cursor: none;
}
/* ==========================================================================
Sliders
========================================================================== */
.media-default-skin--video .media-slider__track {
background-color: oklch(1 0 0 / 0.2);
box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05);
}
.media-default-skin--video .media-slider__preview {
--media-preview-max-width: 11rem;
--media-preview-padding: -1.125rem;
/**
Inset is the difference between the container width and the slider (100%) width.
Divided by 2 as we render the time on both sides.
*/
--media-preview-inset: calc((100cqi - 100%) / 2);
position: absolute;
bottom: calc(100% + 1.2rem);
left: clamp(
calc(var(--media-preview-max-width) / 2 + var(--media-preview-padding) - var(--media-preview-inset)),
var(--media-slider-pointer),
calc(100% - var(--media-preview-max-width) / 2 - var(--media-preview-padding) + var(--media-preview-inset))
);
pointer-events: none;
opacity: 0;
filter: blur(8px);
transform-origin: bottom;
scale: 0.8;
translate: -50%;
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: scale, opacity, filter;
& .media-preview__thumbnail {
max-width: var(--media-preview-max-width);
}
&:has(.media-preview__thumbnail[data-loading]) {
max-height: 6rem;
}
}
.media-default-skin--video .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
opacity: 1;
filter: blur(0);
scale: 1;
}
<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/video-minimal-ui.js"></script>
<link rel="stylesheet" href="./player.css">
<video-player>
<media-container class="media-minimal-skin media-minimal-skin--video">
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" playsinline></video>
<media-poster>
<img src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.webp" />
</media-poster>
<media-buffering-indicator class="media-buffering-indicator">
<media-icon name="spinner" family="minimal" class="media-icon"></media-icon>
</media-buffering-indicator>
<media-error-dialog class="media-error">
<div class="media-error__dialog">
<div class="media-error__content">
<media-alert-dialog-title class="media-error__title">Something went wrong.</media-alert-dialog-title>
<media-alert-dialog-description class="media-error__description"></media-alert-dialog-description>
</div>
<div class="media-error__actions">
<media-alert-dialog-close class="media-button media-button--primary">OK</media-alert-dialog-close>
</div>
</div>
</media-error-dialog>
<media-controls class="media-controls">
<media-tooltip-group>
<div class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
<media-icon name="restart" family="minimal" class="media-icon media-icon--restart"></media-icon>
<media-icon name="play" family="minimal" class="media-icon media-icon--play"></media-icon>
<media-icon name="pause" family="minimal" class="media-icon media-icon--pause"></media-icon>
</media-play-button>
<media-tooltip id="play-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-seek-button commandfor="seek-backward-tooltip" seconds="-10" class="media-button media-button--subtle media-button--icon media-button--seek">
<span class="media-icon__container">
<media-icon name="seek" family="minimal" class="media-icon media-icon--flipped"></media-icon>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-backward-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-seek-button commandfor="seek-forward-tooltip" seconds="10" class="media-button media-button--subtle media-button--icon media-button--seek">
<span class="media-icon__container">
<media-icon name="seek" family="minimal" class="media-icon"></media-icon>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-forward-tooltip" side="top" class="media-tooltip"></media-tooltip>
</div>
<div class="media-time-controls">
<media-time-group class="media-time-group">
<media-time type="current" class="media-time media-time--current"></media-time>
<media-time-separator class="media-time-separator"></media-time-separator>
<media-time type="duration" class="media-time media-time--duration"></media-time>
</media-time-group>
<media-time-slider class="media-slider">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
<div class="media-preview media-slider__preview">
<div class="media-preview__thumbnail-wrapper">
<media-slider-thumbnail class="media-preview__thumbnail"></media-slider-thumbnail>
</div>
<media-slider-value type="pointer" class="media-time media-preview__time"></media-slider-value>
<media-icon name="spinner" family="minimal" class="media-preview__spinner media-icon"></media-icon>
</div>
</media-time-slider>
</div>
<div class="media-button-group">
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--subtle media-button--icon media-button--playback-rate"></media-playback-rate-button>
<media-tooltip id="playback-rate-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--subtle media-button--icon media-button--mute">
<media-icon name="volume-off" family="minimal" class="media-icon media-icon--volume-off"></media-icon>
<media-icon name="volume-low" family="minimal" class="media-icon media-icon--volume-low"></media-icon>
<media-icon name="volume-high" family="minimal" class="media-icon media-icon--volume-high"></media-icon>
</media-mute-button>
<media-popover id="video-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-popover media-popover--volume">
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
</media-volume-slider>
</media-popover>
<media-captions-button commandfor="captions-tooltip" class="media-button media-button--subtle media-button--icon media-button--captions">
<media-icon name="captions-off" family="minimal" class="media-icon media-icon--captions-off"></media-icon>
<media-icon name="captions-on" family="minimal" class="media-icon media-icon--captions-on"></media-icon>
</media-captions-button>
<media-tooltip id="captions-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-cast-button commandfor="cast-tooltip" class="media-button media-button--subtle media-button--icon media-button--cast">
<media-icon name="cast-enter" family="minimal" class="media-icon media-icon--cast-enter"></media-icon>
<media-icon name="cast-exit" family="minimal" class="media-icon media-icon--cast-exit"></media-icon>
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--subtle media-button--icon media-button--pip">
<media-icon name="pip-enter" family="minimal" class="media-icon media-icon--pip-enter"></media-icon>
<media-icon name="pip-exit" family="minimal" class="media-icon media-icon--pip-exit"></media-icon>
</media-pip-button>
<media-tooltip id="pip-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-fullscreen-button commandfor="fullscreen-tooltip" class="media-button media-button--subtle media-button--icon media-button--fullscreen">
<media-icon name="fullscreen-enter" family="minimal" class="media-icon media-icon--fullscreen-enter"></media-icon>
<media-icon name="fullscreen-exit" family="minimal" class="media-icon media-icon--fullscreen-exit"></media-icon>
</media-fullscreen-button>
<media-tooltip id="fullscreen-tooltip" side="top" class="media-tooltip"></media-tooltip>
</div>
</media-tooltip-group>
</media-controls>
<div class="media-overlay"></div>
<!-- Hotkeys -->
<media-hotkey keys="Space" action="togglePaused"></media-hotkey>
<media-hotkey keys="k" action="togglePaused"></media-hotkey>
<media-hotkey keys="m" action="toggleMuted"></media-hotkey>
<media-hotkey keys="f" action="toggleFullscreen"></media-hotkey>
<media-hotkey keys="c" action="toggleSubtitles"></media-hotkey>
<media-hotkey keys="i" action="togglePictureInPicture"></media-hotkey>
<media-hotkey keys="ArrowRight" action="seekStep" value="5"></media-hotkey>
<media-hotkey keys="ArrowLeft" action="seekStep" value="-5"></media-hotkey>
<media-hotkey keys="l" action="seekStep" value="10"></media-hotkey>
<media-hotkey keys="j" action="seekStep" value="-10"></media-hotkey>
<media-hotkey keys="ArrowUp" action="volumeStep" value="0.05"></media-hotkey>
<media-hotkey keys="ArrowDown" action="volumeStep" value="-0.05"></media-hotkey>
<media-hotkey keys="0-9" action="seekToPercent"></media-hotkey>
<media-hotkey keys="Home" action="seekToPercent" value="0"></media-hotkey>
<media-hotkey keys="End" action="seekToPercent" value="100"></media-hotkey>
<media-hotkey keys=">" action="speedUp"></media-hotkey>
<media-hotkey keys="<" action="speedDown"></media-hotkey>
<!-- Gestures -->
<media-gesture type="tap" action="togglePaused" pointer="mouse" region="center"></media-gesture>
<media-gesture type="tap" action="toggleControls" pointer="touch"></media-gesture>
<media-gesture type="doubletap" action="seekStep" value="-10" region="left"></media-gesture>
<media-gesture type="doubletap" action="toggleFullscreen" region="center"></media-gesture>
<media-gesture type="doubletap" action="seekStep" value="10" region="right"></media-gesture>
<!-- Input Feedback -->
<media-status-announcer></media-status-announcer>
<div class="media-input-feedback">
<media-volume-indicator hidden class="media-input-feedback-island media-input-feedback-island--volume">
<media-volume-indicator-fill class="media-input-feedback-island__content">
<media-icon name="volume-high" family="minimal" class="media-icon media-icon--volume-high"></media-icon>
<media-icon name="volume-low" family="minimal" class="media-icon media-icon--volume-low"></media-icon>
<media-icon name="volume-off" family="minimal" class="media-icon media-icon--volume-off"></media-icon>
<div class="media-input-feedback-island__progress" aria-hidden="true"></div>
<media-volume-indicator-value class="media-input-feedback-island__value"></media-volume-indicator-value>
</media-volume-indicator-fill>
</media-volume-indicator>
<media-status-indicator hidden actions="toggleSubtitles toggleFullscreen togglePictureInPicture" class="media-input-feedback-island media-input-feedback-island--status">
<div class="media-input-feedback-island__content">
<media-icon name="captions-on" family="minimal" class="media-icon media-icon--captions-on"></media-icon>
<media-icon name="captions-off" family="minimal" class="media-icon media-icon--captions-off"></media-icon>
<media-icon name="fullscreen-enter" family="minimal" class="media-icon media-icon--fullscreen-enter"></media-icon>
<media-icon name="fullscreen-exit" family="minimal" class="media-icon media-icon--fullscreen-exit"></media-icon>
<media-icon name="pip-enter" family="minimal" class="media-icon media-icon--pip-enter"></media-icon>
<media-icon name="pip-exit" family="minimal" class="media-icon media-icon--pip-exit"></media-icon>
<media-status-indicator-value class="media-input-feedback-island__value"></media-status-indicator-value>
</div>
</media-status-indicator>
<media-seek-indicator hidden class="media-input-feedback-bubble">
<media-icon name="chevron" family="minimal" class="media-icon media-icon--seek"></media-icon>
<media-seek-indicator-value class="media-time"></media-seek-indicator-value>
</media-seek-indicator>
<media-status-indicator hidden actions="togglePaused" class="media-input-feedback-bubble">
<media-icon name="play" family="minimal" class="media-icon media-icon--play"></media-icon>
<media-icon name="pause" family="minimal" class="media-icon media-icon--pause"></media-icon>
</media-status-indicator>
</div>
</media-container>
</video-player>/* -------------------------------------------------------------------------- */
/* Global styles for the host document, outside of the Shadow DOM */
/* -------------------------------------------------------------------------- */
video-player,
live-video-player {
display: contents;
}
/*
Required to override any default video and image styles (such as
Tailwind's CSS reset) and ensure they fill the container as expected.
*/
video-player video,
video-player [slot="poster"],
live-video-player video,
live-video-player [slot="poster"] {
display: block;
width: 100%;
height: 100%;
}
video-player video::-webkit-media-text-track-container,
live-video-player video::-webkit-media-text-track-container {
z-index: 1;
font-family: inherit;
scale: 0.98;
translate: 0 var(--media-caption-track-y, 0);
transition: translate var(--media-caption-track-duration, 0) ease-out;
transition-delay: var(--media-caption-track-delay, 0);
}
/* -------------------------------------------------------------------------- */
/* Shared styles for all HTML skins */
/* -------------------------------------------------------------------------- */
media-tooltip-group {
display: contents;
}
:host {
/* `display:grid` fixes a weird issue with Safari when setting aspect-ratio */
display: grid;
width: 100%;
}
/* Hide volume popover when volume control is unsupported (e.g., iOS Safari). */
.media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}
/* ==========================================================================
Reset
========================================================================== */
.media-minimal-skin *,
.media-minimal-skin *::before,
.media-minimal-skin *::after {
box-sizing: border-box;
}
.media-minimal-skin img,
.media-minimal-skin video,
.media-minimal-skin svg {
display: block;
max-width: 100%;
}
.media-minimal-skin button {
font: inherit;
}
.media-minimal-skin [hidden][hidden] {
/* Keep authored templates hidden even when component classes set display. */
display: none;
}
@media (prefers-reduced-motion: no-preference) {
.media-minimal-skin {
interpolate-size: allow-keywords;
}
}
/* ==========================================================================
Root Container
========================================================================== */
.media-minimal-skin {
--media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
--media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
--media-icon-size: 18px;
position: relative;
display: block;
width: 100%;
height: 100%;
container: media-root / inline-size;
font-family:
Inter Variable,
Inter,
ui-sans-serif,
system-ui,
sans-serif;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
line-height: 1.5;
letter-spacing: normal;
outline: 2px solid transparent;
outline-offset: -4px;
border-radius: var(--media-border-radius, 0.75rem);
isolation: isolate;
transition-timing-function: ease-out;
transition-duration: 100ms;
transition-property: outline-offset, outline-color;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
& > * {
font-size: 0.75rem; /* 12px at 100% font size */
@container media-root (width > 48rem) {
font-size: 0.875rem; /* 14px at 100% font size */
}
}
}
/* ==========================================================================
Media Element
========================================================================== */
.media-minimal-skin ::slotted(video),
.media-minimal-skin video {
display: block;
width: 100%;
height: 100%;
object-fit: var(--media-object-fit, contain);
object-position: var(--media-object-position, center);
}
.media-minimal-skin ::slotted(video) {
border-radius: var(--media-video-border-radius);
}
.media-minimal-skin video {
border-radius: inherit;
}
.media-minimal-skin:fullscreen ::slotted(video),
.media-minimal-skin:fullscreen video {
object-fit: contain;
}
/* ==========================================================================
Overlay / Scrim
========================================================================== */
.media-minimal-skin .media-overlay {
position: absolute;
inset: 0;
pointer-events: none;
background-image: linear-gradient(to top, oklch(0 0 0 / 0.7), oklch(0 0 0 / 0.5) 7.5rem, oklch(0 0 0 / 0));
border-radius: inherit;
opacity: 0;
backdrop-filter: blur(0) saturate(1);
transition-timing-function: ease-out;
transition-duration: var(--media-controls-transition-duration);
transition-property: opacity, backdrop-filter;
}
.media-minimal-skin .media-error ~ .media-overlay {
transition-delay: var(--media-error-dialog-transition-delay);
transition-duration: var(--media-error-dialog-transition-duration);
}
.media-minimal-skin .media-controls[data-visible] ~ .media-overlay,
.media-minimal-skin .media-error[data-open] ~ .media-overlay {
opacity: 1;
}
.media-minimal-skin .media-error[data-open] ~ .media-overlay {
backdrop-filter: blur(16px) saturate(1.2);
}
/* ==========================================================================
Buffering Indicator
========================================================================== */
.media-minimal-skin .media-buffering-indicator {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
color: oklch(1 0 0);
pointer-events: none;
&:not([data-visible]) {
--media-spinner-animation: none;
}
&[data-visible] {
display: flex;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-minimal-skin .media-error:not([data-open]) {
display: none;
}
.media-minimal-skin .media-error__title {
font-weight: 600;
line-height: 1.25;
}
.media-minimal-skin .media-error__description {
overflow-wrap: anywhere;
opacity: 0.7;
}
.media-minimal-skin .media-error__actions {
display: flex;
gap: 0.5rem;
& > * {
flex: 1;
}
}
.media-minimal-skin .media-error[data-open] ~ .media-controls * {
visibility: hidden;
}
/* ==========================================================================
Controls
========================================================================== */
.media-minimal-skin .media-controls {
display: flex;
align-items: center;
container: media-controls / inline-size;
text-shadow: 0 1px 0 var(--media-current-shadow-color);
background-color: var(--media-controls-background-color);
backdrop-filter: var(--media-controls-backdrop-filter);
}
/* ==========================================================================
Time Controls & Display
========================================================================== */
.media-minimal-skin .media-time-controls {
display: flex;
flex: 1;
flex-direction: row-reverse;
gap: 0.75rem;
align-items: center;
container: media-time-controls / inline-size;
}
.media-minimal-skin .media-time-group {
display: flex;
gap: 0.25rem;
align-items: center;
}
.media-minimal-skin .media-time {
font-variant-numeric: tabular-nums;
}
.media-minimal-skin .media-time--current,
.media-minimal-skin .media-time-separator {
display: none;
}
@container media-root (width > 42rem) {
.media-minimal-skin .media-time-controls {
flex-direction: row;
}
.media-minimal-skin .media-time--duration,
.media-minimal-skin .media-time-separator {
color: oklch(from currentColor l c h / 0.6);
}
.media-minimal-skin .media-time--current,
.media-minimal-skin .media-time-separator {
display: inline;
}
}
/* ==========================================================================
Buttons
========================================================================== */
/* Base button */
.media-minimal-skin .media-button {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
min-height: 0;
padding: 0.5rem 1rem;
text-align: center;
touch-action: manipulation;
cursor: pointer;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
border: none;
border-radius: 0.5rem;
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: background-color, outline-offset, scale;
/* Fix weird jumping when clicking on the buttons in Safari. */
will-change: scale;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
&:active {
scale: 0.98;
}
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
&[data-availability="unavailable"],
&[data-availability="unsupported"] {
display: none;
}
}
@supports (corner-shape: squircle) {
.media-minimal-skin .media-button {
border-radius: 1rem;
corner-shape: squircle;
}
}
/* Primary button variant */
.media-minimal-skin .media-button--primary {
font-weight: 500;
color: oklch(0 0 0);
text-shadow: none;
background: oklch(1 0 0);
}
/* Subtle button variant */
.media-minimal-skin .media-button--subtle {
color: inherit;
text-shadow: inherit;
background: transparent;
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
background: oklch(from currentColor l c h / 0.1);
}
}
/* Icon button variant */
.media-minimal-skin .media-button--icon {
display: grid;
width: 2.375rem;
aspect-ratio: 1;
padding: 0;
&:active {
scale: 0.9;
}
& .media-icon {
grid-area: 1 / 1;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
}
}
/* Seek button */
.media-minimal-skin .media-button--seek {
& .media-icon__label {
position: absolute;
right: -1px;
bottom: -3px;
font-size: 10px; /* Hard coded due to size limitations. */
font-weight: 480;
font-variant-numeric: tabular-nums;
}
&:has(.media-icon--flipped) .media-icon__label {
right: unset;
left: -1px;
}
}
/* Playback rate button */
.media-minimal-skin .media-button--playback-rate {
padding: 0;
&::after {
width: 4ch;
font-variant-numeric: tabular-nums;
content: attr(data-rate) "\00D7";
}
}
/* Live button — wide pill button with a status dot (gray → red at the live
edge) rendered via ::before, and "LIVE" text rendered as the button's own
text content. */
.media-minimal-skin .media-button--live {
display: inline-flex;
gap: 0.4rem;
align-items: center;
width: auto;
aspect-ratio: auto;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
text-transform: uppercase;
letter-spacing: 0.05em;
&::before {
display: inline-block;
flex-shrink: 0;
width: 0.5rem;
height: 0.5rem;
content: "";
background-color: oklch(from currentColor l c h / 0.4);
border-radius: 50%;
transition: background-color 150ms ease-out;
}
&[data-live-edge]::before {
background-color: oklch(0.65 0.22 27);
}
}
/* ==========================================================================
Button Groups
========================================================================== */
.media-minimal-skin .media-button-group {
display: flex;
gap: 0.075rem;
align-items: center;
@container media-root (width > 42rem) {
gap: 0.125rem;
}
}
/* ==========================================================================
Icons
========================================================================== */
.media-minimal-skin .media-icon__container {
position: relative;
}
.media-minimal-skin .media-icon {
flex-shrink: 0;
width: var(--media-icon-size);
height: var(--media-icon-size);
}
.media-minimal-skin .media-icon--flipped {
scale: -1 1;
}
/* ==========================================================================
Poster Image
========================================================================== */
.media-minimal-skin media-poster,
.media-minimal-skin > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
transition: opacity 0.25s;
}
.media-minimal-skin media-poster:not([data-visible]),
.media-minimal-skin > img:not([data-visible]) {
opacity: 0;
}
.media-minimal-skin media-poster ::slotted(img),
.media-minimal-skin media-poster img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: var(--media-object-fit, contain);
object-position: var(--media-object-position, center);
border-radius: var(--media-video-border-radius);
}
.media-minimal-skin > img {
object-fit: var(--media-object-fit, contain);
object-position: var(--media-object-position, center);
border-radius: inherit;
}
.media-minimal-skin:fullscreen media-poster ::slotted(img),
.media-minimal-skin:fullscreen media-poster img,
.media-minimal-skin:fullscreen > img {
object-fit: contain;
}
/* ==========================================================================
Media preview
========================================================================== */
.media-minimal-skin .media-preview {
pointer-events: none;
& .media-preview__thumbnail-wrapper {
position: relative;
background-color: oklch(0 0 0 / 0.9);
border-radius: 0.5rem;
}
& .media-preview__thumbnail {
display: block;
border-radius: inherit;
}
& .media-preview__time {
display: block;
margin-top: 0.5rem;
text-align: center;
}
& .media-overlay {
opacity: 1;
}
& .media-preview__spinner {
position: absolute;
top: 50%;
left: 50%;
opacity: 0;
translate: -50% -50%;
}
& .media-preview__thumbnail,
& .media-preview__spinner {
transition: opacity 150ms ease-out;
}
&:not(:has(.media-preview__thumbnail[data-loading])) {
& .media-preview__spinner {
--media-spinner-animation: none;
}
}
&:has(.media-preview__thumbnail[data-loading]) {
& .media-preview__thumbnail {
opacity: 0;
}
& .media-preview__spinner {
opacity: 1;
}
}
}
/* ==========================================================================
Slider
========================================================================== */
.media-minimal-skin .media-slider {
position: relative;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
border-radius: calc(infinity * 1px);
&[data-orientation="horizontal"] {
width: 100%;
min-width: 5rem;
height: 2rem;
}
&[data-orientation="vertical"] {
width: 2rem;
height: 4.5rem;
}
}
/* Track */
.media-minimal-skin .media-slider__track {
position: relative;
overflow: hidden;
user-select: none;
background-color: oklch(from currentColor l c h / 0.2);
border-radius: inherit;
isolation: isolate;
&[data-orientation="horizontal"] {
width: 100%;
height: 0.1875rem;
}
&[data-orientation="vertical"] {
width: 0.1875rem;
height: 100%;
}
}
/* Thumb */
.media-minimal-skin .media-slider__thumb {
position: absolute;
z-index: 10;
width: 0.75rem;
height: 0.75rem;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
0 1px 3px 0 oklch(0 0 0 / 0.15),
0 1px 2px -1px oklch(0 0 0 / 0.15);
opacity: 0;
transform-origin: center;
scale: 0.7;
translate: -50% -50%;
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: opacity, scale, outline-offset;
&[data-orientation="horizontal"] {
top: 50%;
left: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
top: calc(100% - var(--media-slider-fill));
left: 50%;
}
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
}
.media-minimal-skin .media-slider:hover .media-slider__thumb,
.media-minimal-skin .media-slider:focus-within .media-slider__thumb,
.media-minimal-skin .media-slider__thumb--persistent {
opacity: 1;
scale: 1;
}
/* Shared track fills */
.media-minimal-skin .media-slider__buffer,
.media-minimal-skin .media-slider__fill {
position: absolute;
pointer-events: none;
border-radius: inherit;
}
.media-minimal-skin .media-slider__buffer[data-orientation="horizontal"],
.media-minimal-skin .media-slider__fill[data-orientation="horizontal"] {
inset-block: 0;
left: 0;
}
.media-minimal-skin .media-slider__buffer[data-orientation="vertical"],
.media-minimal-skin .media-slider__fill[data-orientation="vertical"] {
inset-inline: 0;
bottom: 0;
}
/* Buffer */
.media-minimal-skin .media-slider__buffer {
background-color: oklch(from currentColor l c h / 0.2);
transition-timing-function: ease-out;
transition-duration: 0.25s;
&[data-orientation="horizontal"] {
width: var(--media-slider-buffer);
transition-property: width;
}
&[data-orientation="vertical"] {
height: var(--media-slider-buffer);
transition-property: height;
}
}
/* Fill */
.media-minimal-skin .media-slider__fill {
background-color: currentColor;
&[data-orientation="horizontal"] {
width: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
height: var(--media-slider-fill);
}
}
/* Dragging — thumb and fill follow the pointer position */
.media-minimal-skin .media-slider[data-dragging] .media-slider__thumb[data-orientation="horizontal"] {
left: var(--media-slider-pointer);
}
.media-minimal-skin .media-slider[data-dragging] .media-slider__thumb[data-orientation="vertical"] {
top: calc(100% - var(--media-slider-pointer));
}
.media-minimal-skin .media-slider[data-dragging] .media-slider__fill[data-orientation="horizontal"] {
width: var(--media-slider-pointer);
}
.media-minimal-skin .media-slider[data-dragging] .media-slider__fill[data-orientation="vertical"] {
height: var(--media-slider-pointer);
}
/* ==========================================================================
Popups & Animations
========================================================================== */
.media-minimal-skin .media-popover,
.media-minimal-skin .media-tooltip {
margin: 0;
overflow: visible;
color: inherit;
border: 0;
transition-timing-function: var(--media-popup-transition-timing-function);
transition-duration: var(--media-popup-transition-duration);
transition-property: scale, opacity, filter;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
filter: blur(8px);
scale: 0.5;
}
&[data-instant] {
transition-duration: 0ms;
}
&[data-side="top"] {
transform-origin: bottom;
}
&[data-side="bottom"] {
transform-origin: top;
}
&[data-side="left"] {
transform-origin: right;
}
&[data-side="right"] {
transform-origin: left;
}
/* Safe area between trigger and popup */
&::before {
position: absolute;
pointer-events: inherit;
content: "";
}
&[data-side="top"]::before,
&[data-side="bottom"]::before {
inset-inline: 0;
width: 100%;
}
&[data-side="top"]::before {
top: 100%;
}
&[data-side="bottom"]::before {
bottom: 100%;
}
&[data-side="left"]::before,
&[data-side="right"]::before {
inset-block: 0;
height: 100%;
}
&[data-side="left"]::before {
left: 100%;
}
&[data-side="right"]::before {
right: 100%;
}
}
.media-minimal-skin .media-popover {
&[data-side="top"]::before,
&[data-side="bottom"]::before {
height: var(--media-popover-side-offset);
}
&[data-side="left"]::before,
&[data-side="right"]::before {
width: var(--media-popover-side-offset);
}
}
.media-minimal-skin .media-tooltip {
padding: 0.25rem 0.5rem;
font-size: 0.75rem; /* 12px at 100% font size */
color: var(--media-tooltip-text-color);
white-space: nowrap;
background-color: var(--media-tooltip-background-color);
border-radius: 0.5rem;
box-shadow:
0 0 0 1px var(--media-tooltip-border-color),
0 4px 6px -1px oklch(0 0 0 / 0.1),
0 2px 4px -2px oklch(0 0 0 / 0.1);
backdrop-filter: var(--media-tooltip-backdrop-filter);
&[data-side="top"]::before,
&[data-side="bottom"]::before {
height: var(--media-tooltip-side-offset);
}
&[data-side="left"]::before,
&[data-side="right"]::before {
width: var(--media-tooltip-side-offset);
}
}
.media-minimal-skin .media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}
/* ==========================================================================
Native Caption Track
========================================================================== */
.media-minimal-skin {
--media-caption-track-duration: var(--media-controls-transition-duration);
--media-caption-track-delay: 25ms;
--media-caption-track-y: -0.5rem;
&:has(.media-controls[data-visible]) {
--media-caption-track-y: -5rem;
}
@container media-root (width > 42rem) {
&:has(.media-controls[data-visible]) > * {
--media-caption-track-y: -3rem;
}
}
}
.media-minimal-skin video::-webkit-media-text-track-container {
z-index: 1;
font-family: inherit;
scale: 0.98;
translate: 0 var(--media-caption-track-y);
transition: translate var(--media-caption-track-duration) ease-out;
transition-delay: var(--media-caption-track-delay);
}
/* ==========================================================================
Input Feedback
========================================================================== */
.media-minimal-skin .media-input-feedback {
position: absolute;
inset-inline: 0;
top: 0;
bottom: 3.5rem; /* Shift up a little in smaller containers */
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
justify-items: center;
overflow: hidden;
color: var(--media-color-primary, oklch(1 0 0));
pointer-events: none;
border-radius: inherit;
@container media-root (width > 24rem) {
bottom: 0;
}
}
/* --- Feedback islands ------------------------------------------------------- */
.media-minimal-skin .media-input-feedback-island {
position: absolute;
inset-inline: 0;
top: 0;
display: flex;
justify-content: center;
padding-top: 0.75rem;
padding-bottom: 8rem;
color: inherit;
text-shadow: 0 1px 0 var(--media-current-shadow-color);
pointer-events: none;
background-image: linear-gradient(to bottom, oklch(0 0 0 / 0.35), oklch(0 0 0 / 0.2) 3rem, oklch(0 0 0 / 0));
transform-origin: top center;
transition-timing-function: ease-out;
transition-duration: 100ms;
.media-input-feedback-island__content {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0.625rem;
}
.media-icon {
display: none;
flex-shrink: 0;
filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
}
.media-input-feedback-island__value {
margin-left: auto;
}
@media (pointer: fine) {
transition-property: translate, filter, opacity;
will-change: translate, filter, opacity;
}
@media (pointer: coarse) {
transition-property: translate, opacity;
will-change: translate, opacity;
}
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
transition-property: translate, filter, opacity;
}
@media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
.media-input-feedback-island__content {
background: var(--media-controls-background-color);
border-radius: 0.5rem;
}
}
/* Default hidden state */
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transition-timing-function: ease-in;
transition-duration: 400ms;
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
filter: blur(8px);
}
@media (prefers-reduced-motion: no-preference) {
&[data-ending-style] {
translate: 0 -100%;
}
}
}
}
.media-minimal-skin .media-input-feedback-island--volume {
.media-input-feedback-island__content {
width: min(80%, 14rem);
}
.media-input-feedback-island__progress {
--media-progress-fill: var(--media-volume-fill);
width: 100%;
height: 0.1875rem;
background-image: linear-gradient(
to right,
currentColor 0%,
currentColor var(--media-progress-fill),
oklch(from currentColor l c h / 0.2) var(--media-progress-fill),
oklch(from currentColor l c h / 0.2) 100%
);
border-radius: calc(Infinity * 1px);
box-shadow: 0 1px 0 var(--media-current-shadow-color-subtle);
}
}
.media-minimal-skin .media-input-feedback-island--volume[data-level="high"] .media-icon--volume-high,
.media-minimal-skin .media-input-feedback-island--volume[data-level="low"] .media-icon--volume-low,
.media-minimal-skin .media-input-feedback-island--volume[data-level="off"] .media-icon--volume-off {
display: block;
}
.media-minimal-skin .media-input-feedback-island--status[data-status="captions-on"] .media-icon--captions-on,
.media-minimal-skin .media-input-feedback-island--status[data-status="captions-off"] .media-icon--captions-off,
.media-minimal-skin .media-input-feedback-island--status[data-status="fullscreen"] .media-icon--fullscreen-enter,
.media-minimal-skin .media-input-feedback-island--status[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
.media-minimal-skin .media-input-feedback-island--status[data-status="pip"] .media-icon--pip-enter,
.media-minimal-skin .media-input-feedback-island--status[data-status="exit-pip"] .media-icon--pip-exit {
display: block;
}
/* --- Boundary shake ------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
.media-minimal-skin .media-input-feedback-island--volume[data-min] .media-input-feedback-island__content,
.media-minimal-skin .media-input-feedback-island--volume[data-max] .media-input-feedback-island__content {
animation: media-shake 300ms ease-in-out;
}
}
/* --- Bubble ---------------------------------------------------------------- */
.media-minimal-skin .media-input-feedback-bubble {
display: flex;
flex-direction: column;
grid-row: 1;
grid-column: 2; /* default to center for status bubbles and undirected seeks */
align-items: center;
justify-content: center;
padding: 1rem;
transition: opacity 250ms ease-out;
@container media-root (width > 24rem) {
padding: 2rem;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transition-timing-function: ease-in;
transition-duration: 200ms;
}
}
/* Direction placement — seek bubbles move to the side implied by their direction. */
.media-minimal-skin .media-input-feedback-bubble[data-direction="backward"] {
grid-column: 1;
justify-self: left;
}
.media-minimal-skin .media-input-feedback-bubble:not([data-direction]) {
grid-column: 2;
transition-timing-function:
ease-out, linear(0, 0.12 1.5%, 1.35 9.7%, 2.2 13.9%, 3 19.9%, 2.7 21.8%, 0.62 37.5%, 0.96 50.9%, 1);
transition-duration: 600ms;
transition-property: opacity, scale;
@media (prefers-reduced-motion: reduce) {
transition: opacity 100ms ease-out;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
scale: 0.8;
transition-timing-function: ease-in;
transition-duration: 200ms;
}
}
.media-minimal-skin .media-input-feedback-bubble[data-direction="forward"] {
grid-column: 3;
justify-self: right;
}
/* --- Bubble icons ---------------------------------------------------------- */
.media-minimal-skin .media-input-feedback-bubble .media-icon {
display: none;
width: 36px;
height: 36px;
}
/* seek: seek icon, flipped for backward */
.media-minimal-skin .media-input-feedback-bubble[data-direction] .media-icon--seek {
display: block;
}
.media-minimal-skin .media-input-feedback-bubble[data-direction="backward"] .media-icon--seek {
transform: scaleX(-1);
}
@media (prefers-reduced-motion: no-preference) {
.media-minimal-skin
.media-input-feedback-bubble[data-direction="forward"]:not([data-starting-style])
.media-icon--seek {
animation: media-slide-in-forward 300ms ease-in-out;
}
.media-minimal-skin
.media-input-feedback-bubble[data-direction="backward"]:not([data-starting-style])
.media-icon--seek {
animation: media-slide-in-backward 300ms ease-in-out;
}
.media-minimal-skin .media-input-feedback-island--status[data-status]:not([data-starting-style]) .media-icon,
.media-minimal-skin .media-input-feedback-bubble[data-status]:not([data-starting-style]) .media-icon {
animation: media-pop-in 250ms ease-out;
}
}
.media-minimal-skin .media-input-feedback-bubble[data-status="pause"] .media-icon--pause,
.media-minimal-skin .media-input-feedback-bubble[data-status="play"] .media-icon--play {
display: block;
}
/* ==========================================================================
Icon State Visibility for Video Skins
Data-attribute-driven visibility rules for multi-state icon buttons.
Uses :is() with both element selectors (for HTML custom element wrappers)
and class selectors (for React rendered SVG elements).
========================================================================== */
/* --- All icons hidden by default --- */
.media-button--play .media-icon--restart,
.media-button--play .media-icon--play,
.media-button--play .media-icon--pause,
.media-button--mute .media-icon--volume-off,
.media-button--mute .media-icon--volume-low,
.media-button--mute .media-icon--volume-high,
.media-button--fullscreen .media-icon--fullscreen-enter,
.media-button--fullscreen .media-icon--fullscreen-exit,
.media-button--pip .media-icon--pip-enter,
.media-button--pip .media-icon--pip-exit,
.media-button--cast .media-icon--cast-enter,
.media-button--cast .media-icon--cast-exit,
.media-button--captions .media-icon--captions-off,
.media-button--captions .media-icon--captions-on {
display: none;
opacity: 0;
}
/* --- Active icon per state --- */
/* Play: ended → restart */
.media-button--play[data-ended] .media-icon--restart,
/* Play: paused (not ended) → play */
.media-button--play:not([data-ended])[data-paused] .media-icon--play,
/* Play: playing (not paused, not ended) → pause */
.media-button--play:not([data-paused]):not([data-ended]) .media-icon--pause,
/* Mute: muted → volume off */
.media-button--mute[data-muted] .media-icon--volume-off,
/* Mute: volume low (not muted) → volume low */
.media-button--mute:not([data-muted])[data-volume-level="low"] .media-icon--volume-low,
/* Mute: volume high (not muted, not low) → volume high */
.media-button--mute:not([data-muted]):not([data-volume-level="low"]) .media-icon--volume-high,
/* Fullscreen: not fullscreen → enter */
.media-button--fullscreen:not([data-fullscreen]) .media-icon--fullscreen-enter,
/* Fullscreen: fullscreen → exit */
.media-button--fullscreen[data-fullscreen] .media-icon--fullscreen-exit,
/* Picture-in-Picture: not active → enter */
.media-button--pip:not([data-pip]) .media-icon--pip-enter,
/* Picture-in-Picture: active → exit */
.media-button--pip[data-pip] .media-icon--pip-exit,
/* Cast: not connected → enter */
.media-button--cast:not([data-cast-state="connected"]) .media-icon--cast-enter,
/* Cast: connected → exit */
.media-button--cast[data-cast-state="connected"] .media-icon--cast-exit,
/* Captions: not active → captions off */
.media-button--captions:not([data-active]) .media-icon--captions-off,
/* Captions: active → captions on */
.media-button--captions[data-active] .media-icon--captions-on {
display: block;
opacity: 1;
}
/* -------------------------------------------------------------------------- */
/* Global @keyframes for all video skins (CSS & Tailwind) */
/* -------------------------------------------------------------------------- */
@keyframes media-shake {
0%,
100% {
translate: 0 0;
}
20% {
translate: -6px 0;
}
40% {
translate: 4px 0;
}
60% {
translate: -2px 0;
}
80% {
translate: 1px 0;
}
}
@keyframes media-slide-in-forward {
from {
translate: -60% 0;
opacity: 0;
}
}
@keyframes media-slide-in-backward {
from {
translate: 60% 0;
opacity: 0;
}
}
@keyframes media-pop-in {
from {
scale: 0.8;
opacity: 0;
}
}
/* -------------------------------------------------------------------------- */
/* Global @properties for all video skins (CSS & Tailwind) */
/* -------------------------------------------------------------------------- */
@property --media-progress-fill {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
/* ==========================================================================
Root
========================================================================== */
.media-minimal-skin--video {
--media-border-color: oklch(0 0 0 / 0.15);
--media-video-border-radius: var(--media-border-radius, 0.75rem);
--media-controls-background-color: transparent;
--media-controls-transition-duration: 100ms;
--media-controls-transition-timing-function: ease-out;
--media-error-dialog-transition-duration: 150ms;
--media-error-dialog-transition-delay: 100ms;
--media-error-dialog-transition-timing-function: ease-out;
--media-popup-transition-duration: 100ms;
--media-popup-transition-timing-function: ease-out;
--media-tooltip-background-color: oklch(1 0 0 / 0.1);
--media-tooltip-border-color: transparent;
--media-tooltip-backdrop-filter: blur(16px) saturate(1.5);
--media-tooltip-text-color: currentColor;
--media-tooltip-side-offset: 0.5rem;
--media-popover-side-offset: 1.5rem;
overflow: clip;
background: oklch(0 0 0);
@media (prefers-reduced-motion: reduce) {
--media-error-dialog-transition-duration: 50ms;
--media-error-dialog-transition-delay: 0ms;
--media-popup-transition-duration: 0ms;
}
@media (prefers-color-scheme: dark) {
--media-border-color: oklch(1 0 0 / 0.15);
}
@media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
--media-controls-background-color: oklch(0 0 0);
--media-tooltip-background-color: oklch(0 0 0);
}
@container media-root (width > 42rem) {
& > * {
--media-popover-side-offset: 0rem;
}
}
&:has(.media-controls:not([data-visible])) {
/* Slight delay to hide controls on non-touch devices after interaction */
@media (pointer: fine) {
--media-controls-transition-duration: 300ms;
}
@media (pointer: coarse) {
--media-controls-transition-duration: 150ms;
}
@media (prefers-reduced-motion: reduce) {
--media-controls-transition-duration: 50ms;
}
}
/* Inner border ring */
&::after {
position: absolute;
inset: 0;
z-index: 10;
pointer-events: none;
content: "";
border-radius: inherit;
box-shadow: inset 0 0 0 1px var(--media-border-color);
}
/* Fullscreen */
&:fullscreen {
--media-border-radius: 0;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-minimal-skin--video .media-error {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
outline: none;
}
.media-minimal-skin--video .media-error__dialog {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 16rem;
padding: 1rem;
color: oklch(1 0 0);
text-shadow: 0 1px 0 oklch(0 0 0 / 0.5);
pointer-events: auto;
transition-delay: var(--media-error-dialog-transition-delay);
transition-timing-function: var(--media-error-dialog-transition-timing-function);
transition-duration: var(--media-error-dialog-transition-duration);
transition-property: opacity, scale;
}
.media-minimal-skin--video .media-error[data-starting-style] .media-error__dialog,
.media-minimal-skin--video .media-error[data-ending-style] .media-error__dialog {
opacity: 0;
scale: 0.5;
}
.media-minimal-skin--video .media-error[data-ending-style] .media-error__dialog {
transition-delay: 0ms;
}
.media-minimal-skin--video .media-error__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.375rem 0;
}
.media-minimal-skin--video .media-error__title {
font-size: 1.125rem;
}
.media-minimal-skin--video .media-error[data-open] ~ .media-controls {
display: none;
}
/* ==========================================================================
Controls (hide/show behavior)
========================================================================== */
.media-minimal-skin--video .media-controls {
position: absolute;
inset-inline: 0.25rem;
bottom: 0.25rem;
z-index: 10;
flex-wrap: wrap;
column-gap: 0.5rem;
padding: 0.25rem;
color: oklch(1 0 0);
border-radius: 0.75rem;
transition-timing-function: var(--media-controls-transition-timing-function);
transition-duration: var(--media-controls-transition-duration);
@media (pointer: fine) {
transition-property: translate, filter, opacity;
will-change: translate, filter, opacity;
}
@media (pointer: coarse) {
transition-property: translate, opacity;
will-change: translate, opacity;
}
&:not([data-visible]) {
pointer-events: none;
opacity: 0;
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
filter: blur(8px);
}
@media (prefers-reduced-motion: no-preference) {
translate: 0 100%;
}
}
& .media-time-controls {
flex: 0 0 100%;
order: -1;
padding-inline: 0.625rem;
}
& .media-button-group:first-child {
flex: 1;
text-align: left;
}
& .media-button-group:last-child {
flex: 1;
justify-content: end;
}
@container media-root (width > 42rem) {
inset-inline: 0.5rem;
bottom: 0.5rem;
flex-wrap: nowrap;
& .media-time-controls {
flex: 1;
order: unset;
}
& .media-button-group:first-child,
& .media-button-group:last-child {
flex: 0 0 auto;
}
}
}
/* Hide cursor when controls are hidden */
.media-minimal-skin--video:has(.media-controls:not([data-visible])) {
cursor: none;
}
/* ==========================================================================
Sliders
========================================================================== */
.media-minimal-skin--video .media-slider__track {
box-shadow: 0 0 0 1px oklch(0 0 0 / 0.05);
}
/* ==========================================================================
Popups & Animations
========================================================================== */
.media-minimal-skin--video .media-popover--volume {
padding-block: 0.75rem;
background: transparent;
border-radius: 0.75rem;
@media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
background: var(--media-controls-background-color);
}
}
/* ==========================================================================
Slider preview
========================================================================== */
.media-minimal-skin--video .media-slider__preview {
--media-preview-max-width: 11rem;
--media-preview-padding: -0.5rem;
/**
Inset is the difference between the container width and the slider (100%) width.
We only add to the end as we render the time there.
*/
--media-preview-inset: calc(100cqi - 100%);
position: absolute;
bottom: 100%;
left: clamp(
calc(var(--media-preview-max-width) / 2 + var(--media-preview-padding)),
var(--media-slider-pointer),
calc(100% - var(--media-preview-max-width) / 2 - var(--media-preview-padding) + var(--media-preview-inset))
);
opacity: 0;
filter: blur(8px);
transform-origin: bottom;
scale: 0.8;
translate: -50%;
transition-timing-function: ease-out;
transition-duration: 150ms;
transition-property: scale, opacity, filter;
@container media-root (width > 42rem) {
bottom: calc(100% + 0.25rem);
left: var(--media-slider-pointer);
}
& .media-preview__thumbnail-wrapper {
position: relative;
&::after {
position: absolute;
inset: 0;
content: "";
border-radius: inherit;
box-shadow:
0 0 0 1px oklch(0 0 0 / 0.05),
0 1px 3px 0 oklch(0 0 0 / 0.2),
0 1px 2px -1px oklch(0 0 0 / 0.2);
}
}
& .media-preview__thumbnail {
max-width: var(--media-preview-max-width);
}
&:has(.media-preview__thumbnail[data-loading]) {
max-height: 6rem;
}
}
.media-minimal-skin--video .media-slider[data-pointing] .media-slider__preview:has([role="img"]:not([data-hidden])) {
opacity: 1;
filter: blur(0);
scale: 1;
}
CSP
If your application uses a Content Security Policy, you may need to allow additional sources for player features to work correctly.
Common requirements
-
media-srcmust allow your media URLs. -
img-srcmust allow any poster or thumbnail image URLs. -
connect-srcmust allow HLS manifests, playlists, captions, and segment requests when using HLS playback. -
media-src blob:is required when using the HLS player variants, which use MSE-backed playback. -
worker-src blob:is required when using thehls.jsplayer variants. -
style-src 'unsafe-inline'is currently required for some player UI and HTML player styling behavior.
Example
Content-Security-Policy:
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data: blob:;
media-src 'self' https: blob:;
connect-src 'self' https:;
worker-src 'self' blob:;See also
That’s it! You now have a fully functional Video.js player. Go forth and play.
Something not quite right? You can submit an issue and ask for help, or explore other support options.