Assemble your player
Feel at home with your framework, skin, and media source
import { createPlayer, Poster } from '@videojs/react';
import { VideoSkin, Video, videoFeatures } from '@videojs/react/video';
import '@videojs/react/video/skin.css';
const Player = createPlayer({ features: videoFeatures });
export function VideoPlayer() {
return (
<Player.Provider>
<VideoSkin>
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" playsInline />
<Poster src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.webp" />
</VideoSkin>
</Player.Provider>
);
}Take full control
Make your player truly your own with fully-editable components
import { selectError } from '@videojs/core/dom';
import { type ComponentProps, forwardRef, type ReactNode, useRef, type PropsWithChildren, type CSSProperties } from 'react';
import { AlertDialog, Container, usePlayer, BufferingIndicator, CaptionsButton, Controls, FullscreenButton, MuteButton, PiPButton, PlayButton, PlaybackRateButton, Popover, SeekButton, Time, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
export interface ErrorDialogClasses {
root?: string;
dialog?: string;
content?: string;
title?: string;
description?: string;
actions?: string;
close?: string;
}
export function ErrorDialog({ classes }: { classes?: ErrorDialogClasses }): ReactNode {
const errorState = usePlayer(selectError);
const lastError = useRef(errorState?.error);
if (errorState?.error) lastError.current = errorState.error;
if (!errorState) return null;
return (
<AlertDialog.Root
open={!!errorState.error}
onOpenChange={(open) => {
if (!open) errorState.dismissError();
}}
>
<AlertDialog.Popup className={classes?.root}>
<div className={classes?.dialog}>
<div className={classes?.content}>
<AlertDialog.Title className={classes?.title}>Something went wrong.</AlertDialog.Title>
<AlertDialog.Description className={classes?.description}>
{lastError.current?.message ?? 'An error occurred while trying to play the video. Please try again.'}
</AlertDialog.Description>
</div>
<div className={classes?.actions}>
<AlertDialog.Close className={classes?.close}>OK</AlertDialog.Close>
</div>
</div>
</AlertDialog.Popup>
</AlertDialog.Root>
);
}
const SEEK_TIME = 10;
export type VideoSkinProps = PropsWithChildren<{ style?: CSSProperties; className?: string }>;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={['media-button', className].filter(Boolean).join(' ')} {...props} />;
});
const errorClasses = {
root: 'media-error',
dialog: 'media-error__dialog media-surface',
content: 'media-error__content',
title: 'media-error__title',
description: 'media-error__description',
actions: 'media-error__actions',
close: 'media-button',
};
function PlayLabel(): ReactNode {
const paused = usePlayer((s) => Boolean(s.paused));
const ended = usePlayer((s) => Boolean(s.ended));
if (ended) return <>Replay</>;
return paused ? <>Play</> : <>Pause</>;
}
function CaptionsLabel(): ReactNode {
const active = usePlayer((s) => Boolean(s.subtitlesShowing));
return active ? <>Disable captions</> : <>Enable captions</>;
}
function PiPLabel(): ReactNode {
const pip = usePlayer((s) => Boolean(s.pip));
return pip ? <>Exit picture-in-picture</> : <>Enter picture-in-picture</>;
}
function FullscreenLabel(): ReactNode {
const fullscreen = usePlayer((s) => Boolean(s.fullscreen));
return fullscreen ? <>Exit fullscreen</> : <>Enter fullscreen</>;
}
export function VideoSkin(props: VideoSkinProps): ReactNode {
const { children, className, ...rest } = props;
return (
<Container className={['media-default-skin media-default-skin--video', className].filter(Boolean).join(' ')} {...rest}>
{children}
<BufferingIndicator
render={(props) => (
<div {...props} className="media-buffering-indicator">
<div className="media-surface">
<svg className="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true" viewBox="0 0 18 18"><rect width="2" height="5" x="8" y=".5" opacity=".5" rx="1"><animate attributeName="opacity" begin="0s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="12.243" y="2.257" opacity=".45" rx="1" transform="rotate(45 13.243 4.757)"><animate attributeName="opacity" begin="0.125s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="12.5" y="8" opacity=".4" rx="1"><animate attributeName="opacity" begin="0.25s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="10.743" y="12.243" opacity=".35" rx="1" transform="rotate(45 13.243 13.243)"><animate attributeName="opacity" begin="0.375s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="8" y="12.5" opacity=".3" rx="1"><animate attributeName="opacity" begin="0.5s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="3.757" y="10.743" opacity=".25" rx="1" transform="rotate(45 4.757 13.243)"><animate attributeName="opacity" begin="0.625s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x=".5" y="8" opacity=".15" rx="1"><animate attributeName="opacity" begin="0.75s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="2.257" y="3.757" opacity=".1" rx="1" transform="rotate(45 4.757 4.757)"><animate attributeName="opacity" begin="0.875s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect></svg>
</div>
</div>
)}
/>
<ErrorDialog classes={errorClasses} />
<Controls.Root className="media-surface media-controls">
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--play">
<svg className="media-icon media-icon--restart" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M9 17a8 8 0 0 1-8-8h2a6 6 0 1 0 1.287-3.713l1.286 1.286A.25.25 0 0 1 5.396 7H1.25A.25.25 0 0 1 1 6.75V2.604a.25.25 0 0 1 .427-.177l1.438 1.438A8 8 0 1 1 9 17"/><path fill="currentColor" d="m11.61 9.639-3.331 2.07a.826.826 0 0 1-1.15-.266.86.86 0 0 1-.129-.452V6.849C7 6.38 7.374 6 7.834 6c.158 0 .312.045.445.13l3.331 2.071a.858.858 0 0 1 0 1.438"/></svg>
<svg className="media-icon media-icon--play" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="m14.051 10.723-7.985 4.964a1.98 1.98 0 0 1-2.758-.638A2.06 2.06 0 0 1 3 13.964V4.036C3 2.91 3.895 2 5 2c.377 0 .747.109 1.066.313l7.985 4.964a2.057 2.057 0 0 1 .627 2.808c-.16.257-.373.475-.627.637"/></svg>
<svg className="media-icon media-icon--pause" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="5" height="14" x="2" y="2" fill="currentColor" rx="1.75"/><rect width="5" height="14" x="11" y="2" fill="currentColor" rx="1.75"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
<PlayLabel />
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<svg className="media-icon media-icon--seek media-icon--flipped" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.414-1.414a6 6 0 1 1 8.956-7.956l-1.286 1.286a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<svg className="media-icon media-icon--seek" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.414-1.414a6 6 0 1 1 8.956-7.956l-1.286 1.286a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
</Tooltip.Root>
<Time.Group className="media-time">
<Time.Value type="current" className="media-time__value" />
<TimeSlider.Root className="media-slider">
<TimeSlider.Track className="media-slider__track">
<TimeSlider.Fill className="media-slider__fill" />
<TimeSlider.Buffer className="media-slider__buffer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
</TimeSlider.Root>
<Time.Value type="duration" className="media-time__value" />
</Time.Group>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button {...props} className="media-button--icon media-button--playback-rate" />}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<MuteButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--mute">
<svg className="media-icon media-icon--volume-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752M14.5 7.586l-1.768-1.768a1 1 0 1 0-1.414 1.414L13.085 9l-1.767 1.768a1 1 0 0 0 1.414 1.414l1.768-1.768 1.768 1.768a1 1 0 0 0 1.414-1.414L15.914 9l1.768-1.768a1 1 0 0 0-1.414-1.414z"/></svg>
<svg className="media-icon media-icon--volume-low" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
<svg className="media-icon media-icon--volume-high" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15.6 3.3c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4C15.4 5.9 16 7.4 16 9s-.6 3.1-1.8 4.3c-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7"/><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
</Button>
)}
/>
}
/>
<Popover.Popup className="media-surface media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--captions">
<svg className="media-icon media-icon--captions-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="16" height="12" x="1" y="3" stroke="currentColor" strokeWidth="2" rx="3"/><rect width="3" height="2" x="3" y="8" fill="currentColor" rx="1"/><rect width="2" height="2" x="13" y="8" fill="currentColor" rx="1"/><rect width="4" height="2" x="11" y="11" fill="currentColor" rx="1"/><rect width="5" height="2" x="7" y="8" fill="currentColor" rx="1"/><rect width="7" height="2" x="3" y="11" fill="currentColor" rx="1"/></svg>
<svg className="media-icon media-icon--captions-on" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15 2a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM4 11a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2zm8 0a1 1 0 1 0 0 2h2a1 1 0 1 0 0-2zM4 8a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2zm4 0a1 1 0 0 0 0 2h3a1 1 0 1 0 0-2zm6 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
<CaptionsLabel />
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton
render={(props) => (
<Button {...props} className="media-button--icon">
<svg className="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M13 2a4 4 0 0 1 4 4v2.035A3.5 3.5 0 0 0 16.5 8H15V6.273C15 5.018 13.96 4 12.679 4H4.32C3.04 4 2 5.018 2 6.273v5.454C2 12.982 3.04 14 4.321 14H6v1.5q0 .255.035.5H4a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4z"/><rect width="10" height="7" x="8" y="10" fill="currentColor" rx="2"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
<PiPLabel />
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--fullscreen">
<svg className="media-icon media-icon--fullscreen-enter" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M9.57 3.617A1 1 0 0 0 8.646 3H4c-.552 0-1 .449-1 1v4.646a.996.996 0 0 0 1.001 1 1 1 0 0 0 .706-.293l4.647-4.647a1 1 0 0 0 .216-1.089m4.812 4.812a1 1 0 0 0-1.089.217l-4.647 4.647a.998.998 0 0 0 .708 1.706H14c.552 0 1-.449 1-1V9.353a1 1 0 0 0-.618-.924"/></svg>
<svg className="media-icon media-icon--fullscreen-exit" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M7.883 1.93a.99.99 0 0 0-1.09.217L2.146 6.793A.998.998 0 0 0 2.853 8.5H7.5c.551 0 1-.449 1-1V2.854a1 1 0 0 0-.617-.924m7.263 7.57H10.5c-.551 0-1 .449-1 1v4.646a.996.996 0 0 0 1.001 1.001 1 1 0 0 0 .706-.293l4.646-4.646a.998.998 0 0 0-.707-1.707z"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
<FullscreenLabel />
</Tooltip.Popup>
</Tooltip.Root>
</Controls.Root>
{/* <div className="media-captions">
<div className="media-captions__container">
<span className="media-captions__text">An example cue</span>
<span className="media-captions__text">
<p>Another example cue with HTML</p>
</span>
</div>
</div> */}
<div className="media-overlay" />
</Container>
);
}
/* ==========================================================================
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--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,
/* 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;
}
/* ==========================================================================
Tooltip Label State Visibility for Video Skins
Data-attribute-driven visibility rules for multi-state tooltip labels.
Uses adjacent sibling selectors to match button state → tooltip content.
========================================================================== */
/* --- All multi-state labels hidden by default --- */
.media-tooltip-label {
display: none;
}
/* --- Active label per state --- */
/* Play: ended → replay */
.media-button--play[data-ended] + .media-tooltip .media-tooltip-label--replay,
/* Play: paused (not ended) → play */
.media-button--play:not([data-ended])[data-paused] + .media-tooltip
.media-tooltip-label--play,
/* Play: playing (not paused, not ended) → pause */
.media-button--play:not([data-paused]):not([data-ended]) + .media-tooltip
.media-tooltip-label--pause,
/* Fullscreen: not fullscreen → enter */
.media-button--fullscreen:not([data-fullscreen]) + .media-tooltip
.media-tooltip-label--enter-fullscreen,
/* Fullscreen: fullscreen → exit */
.media-button--fullscreen[data-fullscreen] + .media-tooltip
.media-tooltip-label--exit-fullscreen,
/* Captions: not active → enable */
.media-button--captions:not([data-active]) + .media-tooltip
.media-tooltip-label--enable-captions,
/* Captions: active → disable */
.media-button--captions[data-active] + .media-tooltip
.media-tooltip-label--disable-captions,
/* PiP: not in pip → enter */
.media-button--pip:not([data-pip]) + .media-tooltip
.media-tooltip-label--enter-pip,
/* PiP: in pip → exit */
.media-button--pip[data-pip] + .media-tooltip
.media-tooltip-label--exit-pip {
display: block;
}
/* ==========================================================================
Reset
========================================================================== */
.media-default-skin *,
.media-default-skin *::before,
.media-default-skin *::after {
box-sizing: border-box;
margin: 0;
}
.media-default-skin img,
.media-default-skin video,
.media-default-skin svg {
display: block;
max-width: 100%;
}
.media-default-skin button {
font: inherit;
}
@media (prefers-reduced-motion: no-preference) {
.media-default-skin {
interpolate-size: allow-keywords;
}
}
/* ==========================================================================
Root Container
========================================================================== */
.media-default-skin {
position: relative;
isolation: isolate;
display: block;
container: media-root / inline-size;
border-radius: var(--media-border-radius, 2rem);
font-family:
Inter Variable,
Inter,
ui-sans-serif,
system-ui,
sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
letter-spacing: normal;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
&:fullscreen {
border-radius: 0;
}
}
/* ==========================================================================
Surface (shared glass effect for tooltips, popovers, controls)
========================================================================== */
.media-default-skin .media-surface {
background-color: var(--media-surface-background-color);
backdrop-filter: var(--media-surface-backdrop-filter);
box-shadow:
inset 0 0 0 1px var(--media-surface-inner-border-color),
0 1px 3px 0 var(--media-surface-shadow-color),
0 1px 2px -1px var(--media-surface-shadow-color);
/* Inner border ring */
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
border-radius: inherit;
box-shadow: 0 0 0 1px var(--media-surface-outer-border-color);
pointer-events: none;
}
@media (prefers-reduced-transparency: reduce) {
background-color: oklch(from var(--media-surface-background-color) l c h / 0.7);
}
@media (prefers-contrast: more) {
background-color: oklch(from var(--media-surface-background-color) l c h / 0.9);
}
}
/* ==========================================================================
Media Element
========================================================================== */
.media-default-skin ::slotted(video),
.media-default-skin video {
display: block;
width: 100%;
height: 100%;
border-radius: var(--media-border-radius, 2rem);
}
/* ==========================================================================
Poster Image
========================================================================== */
.media-default-skin > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
transition: opacity 0.25s;
pointer-events: none;
&:not([data-visible]) {
opacity: 0;
}
}
/* ==========================================================================
Overlay / Scrim
========================================================================== */
.media-default-skin .media-overlay {
position: absolute;
inset: 0;
border-radius: inherit;
background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
backdrop-filter: blur(0) saturate(1.2) brightness(0.9);
opacity: 0;
transition-property: opacity, backdrop-filter;
transition-duration: 300ms;
transition-delay: 500ms;
transition-timing-function: ease-out;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
}
}
.media-default-skin .media-controls[data-visible] ~ .media-overlay,
.media-default-skin .media-error[data-open] ~ .media-overlay {
opacity: 1;
transition-duration: 150ms;
transition-delay: 0ms;
}
.media-default-skin .media-error[data-open] ~ .media-overlay {
backdrop-filter: blur(8px) saturate(1.2) brightness(0.9);
}
/* ==========================================================================
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;
&[data-visible] {
display: flex;
}
.media-surface {
padding: 0.25rem;
border-radius: 100%;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-default-skin .media-error {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
}
.media-default-skin .media-error__dialog {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 18rem;
padding: 0.75rem;
border-radius: 1.75rem;
color: oklch(1 0 0);
font-size: 0.875rem;
transition-property: opacity, transform;
transition-duration: 500ms;
transition-delay: 100ms;
transition-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
);
/* Simple, fast transition for reduced motion users */
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
}
}
.media-default-skin .media-error[data-starting-style] .media-error__dialog,
.media-default-skin .media-error[data-ending-style] .media-error__dialog {
opacity: 0;
transform: scale(0.5);
}
.media-default-skin .media-error__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0.5rem 0.375rem;
}
.media-default-skin .media-error__title {
font-weight: 600;
line-height: 1.25;
}
.media-default-skin .media-error__description {
opacity: 0.7;
}
.media-default-skin .media-error__actions {
display: flex;
gap: 0.5rem;
& > * {
flex: 1;
}
}
/* ==========================================================================
Controls
========================================================================== */
.media-default-skin .media-controls {
container: media-controls / inline-size;
display: flex;
align-items: center;
gap: 0.075rem;
padding: 0.175rem;
border-radius: calc(infinity * 1px);
--media-controls-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.25));
--media-controls-current-shadow-color-subtle: oklch(
from var(--media-controls-current-shadow-color) l c h /
calc(alpha * 0.4)
);
text-shadow: 0 0 1px var(--media-controls-current-shadow-color);
@container media-root (width > 40rem) {
gap: 0.125rem;
padding: 0.25rem;
}
}
/* ==========================================================================
Time Display
========================================================================== */
.media-default-skin .media-time {
container: media-time / inline-size;
display: flex;
align-items: center;
flex: 1;
gap: 0.75rem;
padding-inline: 0.5rem;
& .media-time__value:first-child {
display: none;
@container media-time (width > 18rem) {
display: block;
}
}
}
.media-default-skin .media-time__value {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Buttons
========================================================================== */
/* Base button */
.media-default-skin .media-button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0.5rem 1rem;
background: oklch(1 0 0);
border: none;
border-radius: calc(infinity * 1px);
outline: 2px solid transparent;
outline-offset: -2px;
color: oklch(0 0 0);
font-weight: 500;
text-align: center;
transition-property: background-color, color, outline-offset, transform;
transition-duration: 150ms;
transition-timing-function: ease-out;
cursor: pointer;
user-select: none;
&:focus-visible {
outline-color: oklch(62.3% 0.214 259.815);
outline-offset: 2px;
}
&[disabled] {
opacity: 0.5;
filter: grayscale(1);
cursor: not-allowed;
}
&[data-availability="unavailable"] {
display: none;
}
}
/* Icon button variant */
.media-default-skin .media-button--icon {
display: grid;
width: 2.125rem;
padding: 0;
aspect-ratio: 1;
background: transparent;
color: inherit;
text-shadow: inherit;
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
background-color: oklch(from currentColor l c h / 0.1);
text-decoration: none;
}
&:active {
transform: scale(0.9);
}
& .media-icon {
filter: drop-shadow(0 1px 0 var(--media-controls-current-shadow-color, oklch(0 0 0 / 0.25)));
}
}
/* Seek button */
.media-default-skin .media-button--seek {
& .media-icon__label {
position: absolute;
right: -1px;
bottom: -3px;
font-size: 0.75em;
font-weight: 480;
font-variant-numeric: tabular-nums;
}
&:has(.media-icon--flipped) .media-icon__label {
right: unset;
left: -1px;
}
@container media-controls (width < 28rem) {
display: none;
}
}
/* Playback rate button */
.media-default-skin .media-button--playback-rate {
padding: 0;
&::after {
content: attr(data-rate) "\00D7";
width: 4ch;
font-variant-numeric: tabular-nums;
}
}
/* ==========================================================================
Icons
========================================================================== */
.media-default-skin .media-icon__container {
position: relative;
}
.media-default-skin .media-icon {
display: block;
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.media-default-skin .media-icon--flipped {
scale: -1 1;
}
/* ==========================================================================
Slider
========================================================================== */
.media-default-skin .media-slider {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
border-radius: calc(infinity * 1px);
outline: none;
&[data-orientation="horizontal"] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
&[data-orientation="vertical"] {
width: 1.25rem;
height: 5rem;
}
}
/* Track */
.media-default-skin .media-slider__track {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: inherit;
user-select: none;
&[data-orientation="horizontal"] {
width: 100%;
height: 0.25rem;
}
&[data-orientation="vertical"] {
width: 0.25rem;
height: 100%;
}
}
/* Thumb */
.media-default-skin .media-slider__thumb {
z-index: 10;
position: absolute;
transform: translate(-50%, -50%);
width: 0.625rem;
height: 0.625rem;
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-controls-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;
transition-property: opacity, height, width, outline-offset;
transition-duration: 150ms;
transition-timing-function: ease-out;
user-select: none;
outline: 4px solid transparent;
outline-offset: -4px;
&[data-orientation="horizontal"] {
top: 50%;
left: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
left: 50%;
top: calc(100% - var(--media-slider-fill));
}
&:hover,
&:focus {
outline-color: oklch(from currentColor l c h / 0.25);
outline-offset: 0;
}
}
.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;
border-radius: inherit;
pointer-events: none;
}
.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-duration: 0.25s;
transition-timing-function: ease-out;
&[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);
}
}
/* Time display within slider */
.media-default-skin .media-slider__time-display {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Popups & Tooltips
========================================================================== */
.media-default-skin .media-popover,
.media-default-skin .media-tooltip {
margin: 0;
border: 0;
color: inherit;
overflow: visible;
transition-property: transform, scale, opacity, filter;
transition-duration: 200ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0);
filter: blur(8px);
}
&[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;
}
}
.media-default-skin .media-popover {
--media-popover-side-offset: 0.5rem;
}
.media-default-skin .media-popover--volume {
padding: 0.625rem 0.25rem;
border-radius: calc(infinity * 1px);
}
.media-default-skin .media-tooltip {
padding: 0.25rem 0.625rem;
border-radius: calc(infinity * 1px);
font-size: 0.75rem;
white-space: nowrap;
--media-tooltip-side-offset: 0.5rem;
}
/* ==========================================================================
Captions
========================================================================== */
.media-default-skin .media-captions {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 20;
font-size: 1rem;
text-wrap: balance;
pointer-events: none;
@container media-root (width > 20rem) {
font-size: 1.5rem;
}
@container media-root (width > 48rem) {
font-size: 1.875rem;
}
@container media-root (width > 80rem) {
font-size: 2.25rem;
}
}
.media-default-skin .media-captions__container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 42ch;
margin: 0 auto;
text-align: center;
}
.media-default-skin .media-captions__text {
display: block;
padding: 0.125rem 0.5rem;
color: oklch(1 0 0);
text-shadow:
0 0 1px oklch(0 0 0 / 0.7),
0 0 8px oklch(0 0 0 / 0.7);
text-align: center;
white-space: pre-wrap;
line-height: 1.2;
@media (prefers-contrast: more) {
background: oklch(0 0 0 / 0.7);
text-shadow: none;
box-decoration-break: clone;
}
& > * {
display: inline;
}
}
/* Caption shifting styles (custom and native) */
.media-default-skin {
--media-caption-track-delay: 600ms;
--media-caption-track-y: -0.5rem;
&:has(.media-controls[data-visible]) {
--media-caption-track-delay: 25ms;
--media-caption-track-y: -3.5rem;
}
}
.media-default-skin .media-captions,
.media-default-skin video::-webkit-media-text-track-container {
/* NOTE: The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: var(--media-caption-track-delay);
}
.media-default-skin video::-webkit-media-text-track-container {
transform: translateY(var(--media-caption-track-y)) scale(0.98);
z-index: 1;
font-family: inherit;
}
/* When controls are visible, shift captions up to avoid overlap */
.media-default-skin .media-controls[data-visible] ~ .media-captions {
transform: translateY(calc(var(--media-caption-track-y) - 0.5rem));
}
@media (prefers-reduced-motion: reduce) {
.media-default-skin .media-captions,
.media-default-skin video::-webkit-media-text-track-container {
transition-duration: 50ms;
}
}
/* ==========================================================================
Root
========================================================================== */
.media-default-skin--video {
background: oklch(0 0 0);
--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(64px) brightness(0.9) saturate(1.5);
@media (prefers-color-scheme: dark) {
--media-border-color: oklch(1 0 0 / 0.1);
}
/* Inner border ring */
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
border-radius: inherit;
box-shadow: inset 0 0 0 1px var(--media-border-color);
pointer-events: none;
}
}
/* ==========================================================================
Controls (hide/show behavior)
========================================================================== */
.media-default-skin--video .media-controls {
position: absolute;
bottom: 0.75rem;
inset-inline: 0.75rem;
z-index: 10;
will-change: scale, transform, filter, opacity;
transition-property: scale, transform, filter, opacity;
transition-duration: 100ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
transform-origin: bottom;
color: oklch(1 0 0);
&:not([data-visible]) {
opacity: 0;
scale: 0.9;
filter: blur(8px);
transition-duration: 300ms;
transition-delay: 500ms;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
scale: 1;
filter: blur(0);
transition-duration: 100ms;
}
}
}
/* ==========================================================================
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-container class="media-default-skin media-default-skin--video">
<slot name="media"></slot>
<media-buffering-indicator class="media-buffering-indicator">
<div class="media-surface">
<svg class="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true" viewBox="0 0 18 18"><rect width="2" height="5" x="8" y=".5" opacity=".5" rx="1"><animate attributeName="opacity" begin="0s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="12.243" y="2.257" opacity=".45" rx="1" transform="rotate(45 13.243 4.757)"><animate attributeName="opacity" begin="0.125s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="12.5" y="8" opacity=".4" rx="1"><animate attributeName="opacity" begin="0.25s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="10.743" y="12.243" opacity=".35" rx="1" transform="rotate(45 13.243 13.243)"><animate attributeName="opacity" begin="0.375s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="8" y="12.5" opacity=".3" rx="1"><animate attributeName="opacity" begin="0.5s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="3.757" y="10.743" opacity=".25" rx="1" transform="rotate(45 4.757 13.243)"><animate attributeName="opacity" begin="0.625s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x=".5" y="8" opacity=".15" rx="1"><animate attributeName="opacity" begin="0.75s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="2.257" y="3.757" opacity=".1" rx="1" transform="rotate(45 4.757 4.757)"><animate attributeName="opacity" begin="0.875s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect></svg>
</div>
</media-buffering-indicator>
<media-controls class="media-surface media-controls">
<media-play-button commandfor="play-tooltip" class="media-button media-button--icon media-button--play">
<svg class="media-icon media-icon--restart" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M9 17a8 8 0 0 1-8-8h2a6 6 0 1 0 1.287-3.713l1.286 1.286A.25.25 0 0 1 5.396 7H1.25A.25.25 0 0 1 1 6.75V2.604a.25.25 0 0 1 .427-.177l1.438 1.438A8 8 0 1 1 9 17"/><path fill="currentColor" d="m11.61 9.639-3.331 2.07a.826.826 0 0 1-1.15-.266.86.86 0 0 1-.129-.452V6.849C7 6.38 7.374 6 7.834 6c.158 0 .312.045.445.13l3.331 2.071a.858.858 0 0 1 0 1.438"/></svg>
<svg class="media-icon media-icon--play" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="m14.051 10.723-7.985 4.964a1.98 1.98 0 0 1-2.758-.638A2.06 2.06 0 0 1 3 13.964V4.036C3 2.91 3.895 2 5 2c.377 0 .747.109 1.066.313l7.985 4.964a2.057 2.057 0 0 1 .627 2.808c-.16.257-.373.475-.627.637"/></svg>
<svg class="media-icon media-icon--pause" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="5" height="14" x="2" y="2" fill="currentColor" rx="1.75"/><rect width="5" height="14" x="11" y="2" fill="currentColor" rx="1.75"/></svg>
</media-play-button>
<media-tooltip id="play-tooltip" side="top" class="media-surface media-tooltip">
<span class="media-tooltip-label media-tooltip-label--replay">Replay</span>
<span class="media-tooltip-label media-tooltip-label--play">Play</span>
<span class="media-tooltip-label media-tooltip-label--pause">Pause</span>
</media-tooltip>
<media-seek-button commandfor="seek-backward-tooltip" seconds="-10" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
<svg class="media-icon media-icon--flipped" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.414-1.414a6 6 0 1 1 8.956-7.956l-1.286 1.286a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-backward-tooltip" side="top" class="media-surface media-tooltip">
Seek backward 10 seconds
</media-tooltip>
<media-seek-button commandfor="seek-forward-tooltip" seconds="10" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
<svg class="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.414-1.414a6 6 0 1 1 8.956-7.956l-1.286 1.286a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-forward-tooltip" side="top" class="media-surface media-tooltip">
Seek forward 10 seconds
</media-tooltip>
<media-time-group class="media-time">
<media-time type="current" class="media-time__value"></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>
</media-time-slider>
<media-time type="duration" class="media-time__value"></media-time>
</media-time-group>
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--icon media-button--playback-rate"></media-playback-rate-button>
<media-tooltip id="playback-rate-tooltip" side="top" class="media-surface media-tooltip">
Toggle playback rate
</media-tooltip>
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--icon media-button--mute">
<svg class="media-icon media-icon--volume-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752M14.5 7.586l-1.768-1.768a1 1 0 1 0-1.414 1.414L13.085 9l-1.767 1.768a1 1 0 0 0 1.414 1.414l1.768-1.768 1.768 1.768a1 1 0 0 0 1.414-1.414L15.914 9l1.768-1.768a1 1 0 0 0-1.414-1.414z"/></svg>
<svg class="media-icon media-icon--volume-low" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
<svg class="media-icon media-icon--volume-high" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15.6 3.3c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4C15.4 5.9 16 7.4 16 9s-.6 3.1-1.8 4.3c-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7"/><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
</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--icon media-button--captions">
<svg class="media-icon media-icon--captions-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="16" height="12" x="1" y="3" stroke="currentColor" stroke-width="2" rx="3"/><rect width="3" height="2" x="3" y="8" fill="currentColor" rx="1"/><rect width="2" height="2" x="13" y="8" fill="currentColor" rx="1"/><rect width="4" height="2" x="11" y="11" fill="currentColor" rx="1"/><rect width="5" height="2" x="7" y="8" fill="currentColor" rx="1"/><rect width="7" height="2" x="3" y="11" fill="currentColor" rx="1"/></svg>
<svg class="media-icon media-icon--captions-on" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15 2a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM4 11a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2zm8 0a1 1 0 1 0 0 2h2a1 1 0 1 0 0-2zM4 8a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2zm4 0a1 1 0 0 0 0 2h3a1 1 0 1 0 0-2zm6 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2"/></svg>
</media-captions-button>
<media-tooltip id="captions-tooltip" side="top" class="media-surface media-tooltip">
<span class="media-tooltip-label media-tooltip-label--enable-captions">Enable captions</span>
<span class="media-tooltip-label media-tooltip-label--disable-captions">Disable captions</span>
</media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--icon">
<svg class="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M13 2a4 4 0 0 1 4 4v2.035A3.5 3.5 0 0 0 16.5 8H15V6.273C15 5.018 13.96 4 12.679 4H4.32C3.04 4 2 5.018 2 6.273v5.454C2 12.982 3.04 14 4.321 14H6v1.5q0 .255.035.5H4a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4z"/><rect width="10" height="7" x="8" y="10" fill="currentColor" rx="2"/></svg>
</media-pip-button>
<media-tooltip id="pip-tooltip" side="top" class="media-surface media-tooltip">
<span class="media-tooltip-label media-tooltip-label--enter-pip">Enter picture-in-picture</span>
<span class="media-tooltip-label media-tooltip-label--exit-pip">Exit picture-in-picture</span>
</media-tooltip>
<media-fullscreen-button commandfor="fullscreen-tooltip" class="media-button media-button--icon media-button--fullscreen">
<svg class="media-icon media-icon--fullscreen-enter" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M9.57 3.617A1 1 0 0 0 8.646 3H4c-.552 0-1 .449-1 1v4.646a.996.996 0 0 0 1.001 1 1 1 0 0 0 .706-.293l4.647-4.647a1 1 0 0 0 .216-1.089m4.812 4.812a1 1 0 0 0-1.089.217l-4.647 4.647a.998.998 0 0 0 .708 1.706H14c.552 0 1-.449 1-1V9.353a1 1 0 0 0-.618-.924"/></svg>
<svg class="media-icon media-icon--fullscreen-exit" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M7.883 1.93a.99.99 0 0 0-1.09.217L2.146 6.793A.998.998 0 0 0 2.853 8.5H7.5c.551 0 1-.449 1-1V2.854a1 1 0 0 0-.617-.924m7.263 7.57H10.5c-.551 0-1 .449-1 1v4.646a.996.996 0 0 0 1.001 1.001 1 1 0 0 0 .706-.293l4.646-4.646a.998.998 0 0 0-.707-1.707z"/></svg>
</media-fullscreen-button>
<media-tooltip id="fullscreen-tooltip" side="top" class="media-surface media-tooltip">
<span class="media-tooltip-label media-tooltip-label--enter-fullscreen">Enter fullscreen</span>
<span class="media-tooltip-label media-tooltip-label--exit-fullscreen">Exit fullscreen</span>
</media-tooltip>
</media-controls>
<div class="media-overlay"></div>
</media-container>/* ==========================================================================
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--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,
/* 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;
}
/* ==========================================================================
Tooltip Label State Visibility for Video Skins
Data-attribute-driven visibility rules for multi-state tooltip labels.
Uses adjacent sibling selectors to match button state → tooltip content.
========================================================================== */
/* --- All multi-state labels hidden by default --- */
.media-tooltip-label {
display: none;
}
/* --- Active label per state --- */
/* Play: ended → replay */
.media-button--play[data-ended] + .media-tooltip .media-tooltip-label--replay,
/* Play: paused (not ended) → play */
.media-button--play:not([data-ended])[data-paused] + .media-tooltip
.media-tooltip-label--play,
/* Play: playing (not paused, not ended) → pause */
.media-button--play:not([data-paused]):not([data-ended]) + .media-tooltip
.media-tooltip-label--pause,
/* Fullscreen: not fullscreen → enter */
.media-button--fullscreen:not([data-fullscreen]) + .media-tooltip
.media-tooltip-label--enter-fullscreen,
/* Fullscreen: fullscreen → exit */
.media-button--fullscreen[data-fullscreen] + .media-tooltip
.media-tooltip-label--exit-fullscreen,
/* Captions: not active → enable */
.media-button--captions:not([data-active]) + .media-tooltip
.media-tooltip-label--enable-captions,
/* Captions: active → disable */
.media-button--captions[data-active] + .media-tooltip
.media-tooltip-label--disable-captions,
/* PiP: not in pip → enter */
.media-button--pip:not([data-pip]) + .media-tooltip
.media-tooltip-label--enter-pip,
/* PiP: in pip → exit */
.media-button--pip[data-pip] + .media-tooltip
.media-tooltip-label--exit-pip {
display: block;
}
/* ==========================================================================
Reset
========================================================================== */
.media-default-skin *,
.media-default-skin *::before,
.media-default-skin *::after {
box-sizing: border-box;
margin: 0;
}
.media-default-skin img,
.media-default-skin video,
.media-default-skin svg {
display: block;
max-width: 100%;
}
.media-default-skin button {
font: inherit;
}
@media (prefers-reduced-motion: no-preference) {
.media-default-skin {
interpolate-size: allow-keywords;
}
}
/* ==========================================================================
Root Container
========================================================================== */
.media-default-skin {
position: relative;
isolation: isolate;
display: block;
container: media-root / inline-size;
border-radius: var(--media-border-radius, 2rem);
font-family:
Inter Variable,
Inter,
ui-sans-serif,
system-ui,
sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
letter-spacing: normal;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
&:fullscreen {
border-radius: 0;
}
}
/* ==========================================================================
Surface (shared glass effect for tooltips, popovers, controls)
========================================================================== */
.media-default-skin .media-surface {
background-color: var(--media-surface-background-color);
backdrop-filter: var(--media-surface-backdrop-filter);
box-shadow:
inset 0 0 0 1px var(--media-surface-inner-border-color),
0 1px 3px 0 var(--media-surface-shadow-color),
0 1px 2px -1px var(--media-surface-shadow-color);
/* Inner border ring */
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
border-radius: inherit;
box-shadow: 0 0 0 1px var(--media-surface-outer-border-color);
pointer-events: none;
}
@media (prefers-reduced-transparency: reduce) {
background-color: oklch(from var(--media-surface-background-color) l c h / 0.7);
}
@media (prefers-contrast: more) {
background-color: oklch(from var(--media-surface-background-color) l c h / 0.9);
}
}
/* ==========================================================================
Media Element
========================================================================== */
.media-default-skin ::slotted(video),
.media-default-skin video {
display: block;
width: 100%;
height: 100%;
border-radius: var(--media-border-radius, 2rem);
}
/* ==========================================================================
Poster Image
========================================================================== */
.media-default-skin > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
transition: opacity 0.25s;
pointer-events: none;
&:not([data-visible]) {
opacity: 0;
}
}
/* ==========================================================================
Overlay / Scrim
========================================================================== */
.media-default-skin .media-overlay {
position: absolute;
inset: 0;
border-radius: inherit;
background-image: linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3), oklch(0 0 0 / 0));
backdrop-filter: blur(0) saturate(1.2) brightness(0.9);
opacity: 0;
transition-property: opacity, backdrop-filter;
transition-duration: 300ms;
transition-delay: 500ms;
transition-timing-function: ease-out;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
}
}
.media-default-skin .media-controls[data-visible] ~ .media-overlay,
.media-default-skin .media-error[data-open] ~ .media-overlay {
opacity: 1;
transition-duration: 150ms;
transition-delay: 0ms;
}
.media-default-skin .media-error[data-open] ~ .media-overlay {
backdrop-filter: blur(8px) saturate(1.2) brightness(0.9);
}
/* ==========================================================================
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;
&[data-visible] {
display: flex;
}
.media-surface {
padding: 0.25rem;
border-radius: 100%;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-default-skin .media-error {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
}
.media-default-skin .media-error__dialog {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 18rem;
padding: 0.75rem;
border-radius: 1.75rem;
color: oklch(1 0 0);
font-size: 0.875rem;
transition-property: opacity, transform;
transition-duration: 500ms;
transition-delay: 100ms;
transition-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
);
/* Simple, fast transition for reduced motion users */
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
}
}
.media-default-skin .media-error[data-starting-style] .media-error__dialog,
.media-default-skin .media-error[data-ending-style] .media-error__dialog {
opacity: 0;
transform: scale(0.5);
}
.media-default-skin .media-error__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0.5rem 0.375rem;
}
.media-default-skin .media-error__title {
font-weight: 600;
line-height: 1.25;
}
.media-default-skin .media-error__description {
opacity: 0.7;
}
.media-default-skin .media-error__actions {
display: flex;
gap: 0.5rem;
& > * {
flex: 1;
}
}
/* ==========================================================================
Controls
========================================================================== */
.media-default-skin .media-controls {
container: media-controls / inline-size;
display: flex;
align-items: center;
gap: 0.075rem;
padding: 0.175rem;
border-radius: calc(infinity * 1px);
--media-controls-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.25));
--media-controls-current-shadow-color-subtle: oklch(
from var(--media-controls-current-shadow-color) l c h /
calc(alpha * 0.4)
);
text-shadow: 0 0 1px var(--media-controls-current-shadow-color);
@container media-root (width > 40rem) {
gap: 0.125rem;
padding: 0.25rem;
}
}
/* ==========================================================================
Time Display
========================================================================== */
.media-default-skin .media-time {
container: media-time / inline-size;
display: flex;
align-items: center;
flex: 1;
gap: 0.75rem;
padding-inline: 0.5rem;
& .media-time__value:first-child {
display: none;
@container media-time (width > 18rem) {
display: block;
}
}
}
.media-default-skin .media-time__value {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Buttons
========================================================================== */
/* Base button */
.media-default-skin .media-button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0.5rem 1rem;
background: oklch(1 0 0);
border: none;
border-radius: calc(infinity * 1px);
outline: 2px solid transparent;
outline-offset: -2px;
color: oklch(0 0 0);
font-weight: 500;
text-align: center;
transition-property: background-color, color, outline-offset, transform;
transition-duration: 150ms;
transition-timing-function: ease-out;
cursor: pointer;
user-select: none;
&:focus-visible {
outline-color: oklch(62.3% 0.214 259.815);
outline-offset: 2px;
}
&[disabled] {
opacity: 0.5;
filter: grayscale(1);
cursor: not-allowed;
}
&[data-availability="unavailable"] {
display: none;
}
}
/* Icon button variant */
.media-default-skin .media-button--icon {
display: grid;
width: 2.125rem;
padding: 0;
aspect-ratio: 1;
background: transparent;
color: inherit;
text-shadow: inherit;
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
background-color: oklch(from currentColor l c h / 0.1);
text-decoration: none;
}
&:active {
transform: scale(0.9);
}
& .media-icon {
filter: drop-shadow(0 1px 0 var(--media-controls-current-shadow-color, oklch(0 0 0 / 0.25)));
}
}
/* Seek button */
.media-default-skin .media-button--seek {
& .media-icon__label {
position: absolute;
right: -1px;
bottom: -3px;
font-size: 0.75em;
font-weight: 480;
font-variant-numeric: tabular-nums;
}
&:has(.media-icon--flipped) .media-icon__label {
right: unset;
left: -1px;
}
@container media-controls (width < 28rem) {
display: none;
}
}
/* Playback rate button */
.media-default-skin .media-button--playback-rate {
padding: 0;
&::after {
content: attr(data-rate) "\00D7";
width: 4ch;
font-variant-numeric: tabular-nums;
}
}
/* ==========================================================================
Icons
========================================================================== */
.media-default-skin .media-icon__container {
position: relative;
}
.media-default-skin .media-icon {
display: block;
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.media-default-skin .media-icon--flipped {
scale: -1 1;
}
/* ==========================================================================
Slider
========================================================================== */
.media-default-skin .media-slider {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
border-radius: calc(infinity * 1px);
outline: none;
&[data-orientation="horizontal"] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
&[data-orientation="vertical"] {
width: 1.25rem;
height: 5rem;
}
}
/* Track */
.media-default-skin .media-slider__track {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: inherit;
user-select: none;
&[data-orientation="horizontal"] {
width: 100%;
height: 0.25rem;
}
&[data-orientation="vertical"] {
width: 0.25rem;
height: 100%;
}
}
/* Thumb */
.media-default-skin .media-slider__thumb {
z-index: 10;
position: absolute;
transform: translate(-50%, -50%);
width: 0.625rem;
height: 0.625rem;
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-controls-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;
transition-property: opacity, height, width, outline-offset;
transition-duration: 150ms;
transition-timing-function: ease-out;
user-select: none;
outline: 4px solid transparent;
outline-offset: -4px;
&[data-orientation="horizontal"] {
top: 50%;
left: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
left: 50%;
top: calc(100% - var(--media-slider-fill));
}
&:hover,
&:focus {
outline-color: oklch(from currentColor l c h / 0.25);
outline-offset: 0;
}
}
.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;
border-radius: inherit;
pointer-events: none;
}
.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-duration: 0.25s;
transition-timing-function: ease-out;
&[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);
}
}
/* Time display within slider */
.media-default-skin .media-slider__time-display {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Popups & Tooltips
========================================================================== */
.media-default-skin .media-popover,
.media-default-skin .media-tooltip {
margin: 0;
border: 0;
color: inherit;
overflow: visible;
transition-property: transform, scale, opacity, filter;
transition-duration: 200ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0);
filter: blur(8px);
}
&[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;
}
}
.media-default-skin .media-popover {
--media-popover-side-offset: 0.5rem;
}
.media-default-skin .media-popover--volume {
padding: 0.625rem 0.25rem;
border-radius: calc(infinity * 1px);
}
.media-default-skin .media-tooltip {
padding: 0.25rem 0.625rem;
border-radius: calc(infinity * 1px);
font-size: 0.75rem;
white-space: nowrap;
--media-tooltip-side-offset: 0.5rem;
}
/* ==========================================================================
Captions
========================================================================== */
.media-default-skin .media-captions {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 20;
font-size: 1rem;
text-wrap: balance;
pointer-events: none;
@container media-root (width > 20rem) {
font-size: 1.5rem;
}
@container media-root (width > 48rem) {
font-size: 1.875rem;
}
@container media-root (width > 80rem) {
font-size: 2.25rem;
}
}
.media-default-skin .media-captions__container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 42ch;
margin: 0 auto;
text-align: center;
}
.media-default-skin .media-captions__text {
display: block;
padding: 0.125rem 0.5rem;
color: oklch(1 0 0);
text-shadow:
0 0 1px oklch(0 0 0 / 0.7),
0 0 8px oklch(0 0 0 / 0.7);
text-align: center;
white-space: pre-wrap;
line-height: 1.2;
@media (prefers-contrast: more) {
background: oklch(0 0 0 / 0.7);
text-shadow: none;
box-decoration-break: clone;
}
& > * {
display: inline;
}
}
/* Caption shifting styles (custom and native) */
.media-default-skin {
--media-caption-track-delay: 600ms;
--media-caption-track-y: -0.5rem;
&:has(.media-controls[data-visible]) {
--media-caption-track-delay: 25ms;
--media-caption-track-y: -3.5rem;
}
}
.media-default-skin .media-captions,
.media-default-skin video::-webkit-media-text-track-container {
/* NOTE: The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: var(--media-caption-track-delay);
}
.media-default-skin video::-webkit-media-text-track-container {
transform: translateY(var(--media-caption-track-y)) scale(0.98);
z-index: 1;
font-family: inherit;
}
/* When controls are visible, shift captions up to avoid overlap */
.media-default-skin .media-controls[data-visible] ~ .media-captions {
transform: translateY(calc(var(--media-caption-track-y) - 0.5rem));
}
@media (prefers-reduced-motion: reduce) {
.media-default-skin .media-captions,
.media-default-skin video::-webkit-media-text-track-container {
transition-duration: 50ms;
}
}
/* ==========================================================================
Root
========================================================================== */
.media-default-skin--video {
background: oklch(0 0 0);
--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(64px) brightness(0.9) saturate(1.5);
@media (prefers-color-scheme: dark) {
--media-border-color: oklch(1 0 0 / 0.1);
}
/* Inner border ring */
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
border-radius: inherit;
box-shadow: inset 0 0 0 1px var(--media-border-color);
pointer-events: none;
}
}
/* ==========================================================================
Controls (hide/show behavior)
========================================================================== */
.media-default-skin--video .media-controls {
position: absolute;
bottom: 0.75rem;
inset-inline: 0.75rem;
z-index: 10;
will-change: scale, transform, filter, opacity;
transition-property: scale, transform, filter, opacity;
transition-duration: 100ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
transform-origin: bottom;
color: oklch(1 0 0);
&:not([data-visible]) {
opacity: 0;
scale: 0.9;
filter: blur(8px);
transition-duration: 300ms;
transition-delay: 500ms;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
scale: 1;
filter: blur(0);
transition-duration: 100ms;
}
}
}
/* ==========================================================================
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-container class="media-minimal-skin media-minimal-skin--video">
<slot name="media"></slot>
<media-buffering-indicator class="media-buffering-indicator">
<svg class="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true" viewBox="0 0 18 18"><rect width="2" height="5" x="8" y=".5" opacity=".5" rx="1"><animate attributeName="opacity" begin="0s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="12.243" y="2.257" opacity=".45" rx="1" transform="rotate(45 13.243 4.757)"><animate attributeName="opacity" begin="0.125s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="12.5" y="8" opacity=".4" rx="1"><animate attributeName="opacity" begin="0.25s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="10.743" y="12.243" opacity=".35" rx="1" transform="rotate(45 13.243 13.243)"><animate attributeName="opacity" begin="0.375s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="8" y="12.5" opacity=".3" rx="1"><animate attributeName="opacity" begin="0.5s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="3.757" y="10.743" opacity=".25" rx="1" transform="rotate(45 4.757 13.243)"><animate attributeName="opacity" begin="0.625s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x=".5" y="8" opacity=".15" rx="1"><animate attributeName="opacity" begin="0.75s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="2.257" y="3.757" opacity=".1" rx="1" transform="rotate(45 4.757 4.757)"><animate attributeName="opacity" begin="0.875s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect></svg>
</media-buffering-indicator>
<media-controls class="media-controls">
<span class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--icon media-button--play">
<svg class="media-icon media-icon--restart" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M9 17a8 8 0 0 1-8-8h1.5a6.5 6.5 0 1 0 1.43-4.07l1.643 1.643A.25.25 0 0 1 5.396 7H1.25A.25.25 0 0 1 1 6.75V2.604a.25.25 0 0 1 .427-.177l1.438 1.438A8 8 0 1 1 9 17"/><path fill="currentColor" d="m11.61 9.639-3.331 2.07a.826.826 0 0 1-1.15-.266.86.86 0 0 1-.129-.452V6.849C7 6.38 7.374 6 7.834 6c.158 0 .312.045.445.13l3.331 2.071a.858.858 0 0 1 0 1.438"/></svg>
<svg class="media-icon media-icon--play" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="m13.473 10.476-6.845 4.256a1.697 1.697 0 0 1-2.364-.547 1.77 1.77 0 0 1-.264-.93v-8.51C4 3.78 4.768 3 5.714 3c.324 0 .64.093.914.268l6.845 4.255a1.763 1.763 0 0 1 0 2.953"/></svg>
<svg class="media-icon media-icon--pause" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="4" height="12" x="3" y="3" fill="currentColor" rx="1.75"/><rect width="4" height="12" x="11" y="3" fill="currentColor" rx="1.75"/></svg>
</media-play-button>
<media-tooltip id="play-tooltip" side="top" class="media-tooltip">
<span class="media-tooltip-label media-tooltip-label--replay">Replay</span>
<span class="media-tooltip-label media-tooltip-label--play">Play</span>
<span class="media-tooltip-label media-tooltip-label--pause">Pause</span>
</media-tooltip>
<media-seek-button commandfor="seek-backward-tooltip" seconds="-10" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
<svg class="media-icon media-icon--flipped" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.06-1.06a6.5 6.5 0 1 1 9.665-8.665l-1.641 1.641a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-backward-tooltip" side="top" class="media-tooltip">
Seek backward 10 seconds
</media-tooltip>
<media-seek-button commandfor="seek-forward-tooltip" seconds="10" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
<svg class="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.06-1.06a6.5 6.5 0 1 1 9.665-8.665l-1.641 1.641a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span class="media-icon__label">10</span>
</span>
</media-seek-button>
<media-tooltip id="seek-forward-tooltip" side="top" class="media-tooltip">
Seek forward 10 seconds
</media-tooltip>
</span>
<span class="media-time-controls">
<media-time-group class="media-time">
<media-time type="current" class="media-time__value media-time__value--current"></media-time>
<media-time-separator class="media-time__separator"></media-time-separator>
<media-time type="duration" class="media-time__value media-time__value--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>
</media-time-slider>
</span>
<span class="media-button-group">
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--icon media-button--playback-rate"></media-playback-rate-button>
<media-tooltip id="playback-rate-tooltip" side="top" class="media-tooltip">
Toggle playback rate
</media-tooltip>
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--icon media-button--mute">
<svg class="media-icon media-icon--volume-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752M14.5 7.586l-1.768-1.768a1 1 0 1 0-1.414 1.414L13.085 9l-1.767 1.768a1 1 0 0 0 1.414 1.414l1.768-1.768 1.768 1.768a1 1 0 0 0 1.414-1.414L15.914 9l1.768-1.768a1 1 0 0 0-1.414-1.414z"/></svg>
<svg class="media-icon media-icon--volume-low" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
<svg class="media-icon media-icon--volume-high" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15.6 3.3c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4C15.4 5.9 16 7.4 16 9s-.6 3.1-1.8 4.3c-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7"/><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
</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--icon media-button--captions">
<svg class="media-icon media-icon--captions-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="16.5" height="12.5" x=".75" y="2.75" stroke="currentColor" stroke-width="1.5" rx="3"/><rect width="3" height="1.5" x="3" y="8.5" fill="currentColor" rx=".75"/><rect width="2" height="1.5" x="13" y="8.5" fill="currentColor" rx=".75"/><rect width="4" height="1.5" x="11" y="11.5" fill="currentColor" rx=".75"/><rect width="5" height="1.5" x="7" y="8.5" fill="currentColor" rx=".75"/><rect width="7" height="1.5" x="3" y="11.5" fill="currentColor" rx=".75"/></svg>
<svg class="media-icon media-icon--captions-on" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15 2a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM3.75 11.5a.75.75 0 0 0 0 1.5h5.5a.75.75 0 0 0 0-1.5zm8 0a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5zm-8-3a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5zm4 0a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5zm6 0a.75.75 0 0 0 0 1.5h.5a.75.75 0 0 0 0-1.5z"/></svg>
</media-captions-button>
<media-tooltip id="captions-tooltip" side="top" class="media-tooltip">
Toggle captions
</media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--icon">
<svg class="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M13 2a4 4 0 0 1 4 4v2.645a3.5 3.5 0 0 0-1-.145h-.5V6A2.5 2.5 0 0 0 13 3.5H4A2.5 2.5 0 0 0 1.5 6v6A2.5 2.5 0 0 0 4 14.5h2.5v.5c0 .347.05.683.145 1H4a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4z"/><rect width="10" height="7" x="8" y="10" fill="currentColor" rx="2"/></svg>
</media-pip-button>
<media-tooltip id="pip-tooltip" side="top" class="media-tooltip">
<span class="media-tooltip-label media-tooltip-label--enter-pip">Enter picture-in-picture</span>
<span class="media-tooltip-label media-tooltip-label--exit-pip">Exit picture-in-picture</span>
</media-tooltip>
<media-fullscreen-button commandfor="fullscreen-tooltip" class="media-button media-button--icon media-button--fullscreen">
<svg class="media-icon media-icon--fullscreen-enter" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15.25 2a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0V3.5h-3.75a.75.75 0 0 1-.743-.648L10 2.75a.75.75 0 0 1 .75-.75z"/><path fill="currentColor" d="M14.72 2.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06zM2.75 10a.75.75 0 0 1 .75.75v3.75h3.75a.75.75 0 0 1 .743.648L8 15.25a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-4.5a.75.75 0 0 1 .75-.75"/><path fill="currentColor" d="M6.72 10.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06z"/></svg>
<svg class="media-icon media-icon--fullscreen-exit" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M10.75 2a.75.75 0 0 1 .75.75V6.5h3.75a.75.75 0 0 1 .743.648L16 7.25a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-4.5a.75.75 0 0 1 .75-.75"/><path fill="currentColor" d="M14.72 2.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06zM7.25 10a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0V11.5H2.75a.75.75 0 0 1-.743-.648L2 10.75a.75.75 0 0 1 .75-.75z"/><path fill="currentColor" d="M6.72 10.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06z"/></svg>
</media-fullscreen-button>
<media-tooltip id="fullscreen-tooltip" side="top" class="media-tooltip">
<span class="media-tooltip-label media-tooltip-label--enter-fullscreen">Enter fullscreen</span>
<span class="media-tooltip-label media-tooltip-label--exit-fullscreen">Exit fullscreen</span>
</media-tooltip>
</span>
</media-controls>
<div class="media-overlay"></div>
</media-container>/* ==========================================================================
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--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,
/* 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;
}
/* ==========================================================================
Tooltip Label State Visibility for Video Skins
Data-attribute-driven visibility rules for multi-state tooltip labels.
Uses adjacent sibling selectors to match button state → tooltip content.
========================================================================== */
/* --- All multi-state labels hidden by default --- */
.media-tooltip-label {
display: none;
}
/* --- Active label per state --- */
/* Play: ended → replay */
.media-button--play[data-ended] + .media-tooltip .media-tooltip-label--replay,
/* Play: paused (not ended) → play */
.media-button--play:not([data-ended])[data-paused] + .media-tooltip
.media-tooltip-label--play,
/* Play: playing (not paused, not ended) → pause */
.media-button--play:not([data-paused]):not([data-ended]) + .media-tooltip
.media-tooltip-label--pause,
/* Fullscreen: not fullscreen → enter */
.media-button--fullscreen:not([data-fullscreen]) + .media-tooltip
.media-tooltip-label--enter-fullscreen,
/* Fullscreen: fullscreen → exit */
.media-button--fullscreen[data-fullscreen] + .media-tooltip
.media-tooltip-label--exit-fullscreen,
/* Captions: not active → enable */
.media-button--captions:not([data-active]) + .media-tooltip
.media-tooltip-label--enable-captions,
/* Captions: active → disable */
.media-button--captions[data-active] + .media-tooltip
.media-tooltip-label--disable-captions,
/* PiP: not in pip → enter */
.media-button--pip:not([data-pip]) + .media-tooltip
.media-tooltip-label--enter-pip,
/* PiP: in pip → exit */
.media-button--pip[data-pip] + .media-tooltip
.media-tooltip-label--exit-pip {
display: block;
}
/* ==========================================================================
Reset
========================================================================== */
.media-minimal-skin *,
.media-minimal-skin *::before,
.media-minimal-skin *::after {
box-sizing: border-box;
margin: 0;
}
.media-minimal-skin img,
.media-minimal-skin video,
.media-minimal-skin svg {
display: block;
max-width: 100%;
}
.media-minimal-skin button {
font: inherit;
}
@media (prefers-reduced-motion: no-preference) {
.media-minimal-skin {
interpolate-size: allow-keywords;
}
}
/* ==========================================================================
Root Container
========================================================================== */
.media-minimal-skin {
position: relative;
isolation: isolate;
display: block;
container: media-root / inline-size;
border-radius: var(--media-border-radius, 0.75rem);
font-family:
Inter Variable,
Inter,
ui-sans-serif,
system-ui,
sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
letter-spacing: normal;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
/* ==========================================================================
Media Element
========================================================================== */
.media-minimal-skin ::slotted(video),
.media-minimal-skin video {
display: block;
width: 100%;
height: 100%;
border-radius: var(--media-border-radius, 0.75rem);
}
/* ==========================================================================
Poster Image
========================================================================== */
.media-minimal-skin > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
transition: opacity 0.25s;
pointer-events: none;
&:not([data-visible]) {
opacity: 0;
}
}
/* ==========================================================================
Overlay / Scrim
========================================================================== */
.media-minimal-skin .media-overlay {
position: absolute;
inset: 0;
border-radius: inherit;
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));
backdrop-filter: blur(0) saturate(1.2) brightness(0.9);
opacity: 0;
transition-property: opacity, backdrop-filter;
transition-duration: 500ms;
transition-delay: 500ms;
transition-timing-function: ease-out;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
}
}
.media-minimal-skin .media-controls[data-visible] ~ .media-overlay,
.media-minimal-skin .media-error[data-open] ~ .media-overlay {
opacity: 1;
transition-duration: 150ms;
transition-delay: 0ms;
}
.media-minimal-skin .media-error[data-open] ~ .media-overlay {
backdrop-filter: blur(8px) saturate(1.2) brightness(0.9);
}
/* ==========================================================================
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;
&[data-visible] {
display: flex;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-minimal-skin .media-error {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.media-minimal-skin .media-error__dialog {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 16rem;
padding: 1rem;
color: oklch(1 0 0);
font-size: 0.875rem;
text-shadow: 0 1px 0 oklch(0 0 0 / 0.5);
transition-property: opacity, transform;
transition-duration: 500ms;
transition-delay: 100ms;
transition-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
);
/* Simple, fast transition for reduced motion users */
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
}
}
.media-minimal-skin .media-error[data-starting-style] .media-error__dialog,
.media-minimal-skin .media-error[data-ending-style] .media-error__dialog {
opacity: 0;
transform: scale(0.5);
}
.media-minimal-skin .media-error__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.375rem 0;
}
.media-minimal-skin .media-error__title {
font-weight: 600;
line-height: 1.25;
}
.media-minimal-skin .media-error__description {
opacity: 0.7;
}
.media-minimal-skin .media-error__actions {
display: flex;
gap: 0.5rem;
& > * {
flex: 1;
}
}
/* ==========================================================================
Controls
========================================================================== */
.media-minimal-skin .media-controls {
container: media-controls / inline-size;
display: flex;
align-items: center;
--media-controls-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.25));
--media-controls-current-shadow-color-subtle: oklch(
from var(--media-controls-current-shadow-color) l c h /
calc(alpha * 0.4)
);
text-shadow: 0 0 1px var(--media-controls-current-shadow-color);
}
/* ==========================================================================
Time Controls & Display
========================================================================== */
.media-minimal-skin .media-time-controls {
display: flex;
flex-direction: row-reverse;
align-items: center;
flex: 1;
gap: 0.75rem;
}
.media-minimal-skin .media-time {
display: flex;
align-items: center;
gap: 0.25rem;
}
.media-minimal-skin .media-time__value {
font-variant-numeric: tabular-nums;
}
.media-minimal-skin .media-time__value--current,
.media-minimal-skin .media-time__separator {
display: none;
}
@container media-controls (width > 28rem) {
.media-minimal-skin .media-time-controls {
flex-direction: row;
}
.media-minimal-skin .media-time__value--duration,
.media-minimal-skin .media-time__separator {
color: oklch(from currentColor l c h / 0.6);
}
.media-minimal-skin .media-time__value--current,
.media-minimal-skin .media-time__separator {
display: inline;
}
}
/* ==========================================================================
Button Groups
========================================================================== */
.media-minimal-skin .media-button-group {
display: flex;
align-items: center;
gap: 0.075rem;
@container media-root (width > 40rem) {
gap: 0.125rem;
}
}
/* ==========================================================================
Buttons
========================================================================== */
/* Base button */
.media-minimal-skin .media-button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0.5rem 1rem;
background: oklch(1 0 0);
border: none;
border-radius: 0.5rem;
outline: 2px solid transparent;
outline-offset: -2px;
color: oklch(0 0 0);
font-weight: 500;
text-align: center;
text-shadow: inherit;
transition-property: background-color, color, outline-offset, transform;
transition-duration: 150ms;
transition-timing-function: ease-out;
cursor: pointer;
user-select: none;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
&[disabled] {
opacity: 0.5;
filter: grayscale(1);
cursor: not-allowed;
}
&[data-availability="unavailable"] {
display: none;
}
}
/* Icon button variant */
.media-minimal-skin .media-button--icon {
display: grid;
width: 2.375rem;
padding: 0;
aspect-ratio: 1;
background: transparent;
color: inherit;
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
color: oklch(from currentColor l c h / 0.8);
text-decoration: none;
}
&:active {
transform: scale(0.9);
}
& .media-icon {
filter: drop-shadow(0 1px 0 var(--media-controls-current-shadow-color, oklch(0 0 0 / 0.25)));
}
}
/* Seek button */
.media-minimal-skin .media-button--seek {
& .media-icon__label {
position: absolute;
right: -1px;
bottom: -3px;
font-size: 0.75em;
font-weight: 480;
font-variant-numeric: tabular-nums;
}
&:has(.media-icon--flipped) .media-icon__label {
right: unset;
left: -1px;
}
@container media-controls (width < 28rem) {
display: none;
}
}
/* Playback rate button */
.media-minimal-skin .media-button--playback-rate {
padding: 0;
&::after {
content: attr(data-rate) "\00D7";
width: 4ch;
font-variant-numeric: tabular-nums;
}
}
/* ==========================================================================
Icons
========================================================================== */
.media-minimal-skin .media-icon__container {
position: relative;
}
.media-minimal-skin .media-icon {
display: block;
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.media-minimal-skin .media-icon--flipped {
scale: -1 1;
}
/* ==========================================================================
Slider
========================================================================== */
.media-minimal-skin .media-slider {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
border-radius: calc(infinity * 1px);
outline: none;
&[data-orientation="horizontal"] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
&[data-orientation="vertical"] {
width: 1.25rem;
height: 4.5rem;
}
}
/* Track */
.media-minimal-skin .media-slider__track {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: inherit;
user-select: none;
background-color: oklch(from currentColor l c h / 0.2);
&[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;
transform: translate(-50%, -50%);
z-index: 10;
width: 0.75rem;
height: 0.75rem;
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-controls-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;
scale: 0.7;
transform-origin: center;
transition-property: opacity, scale, outline-offset;
transition-duration: 150ms;
transition-timing-function: ease-out;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
&[data-orientation="horizontal"] {
top: 50%;
left: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
left: 50%;
top: calc(100% - var(--media-slider-fill));
}
&: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;
border-radius: inherit;
pointer-events: none;
}
.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-duration: 0.25s;
transition-timing-function: ease-out;
&[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);
}
}
/* Time display within slider */
.media-minimal-skin .media-slider__time-display {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Popups & Animations
========================================================================== */
.media-minimal-skin .media-popover,
.media-minimal-skin .media-tooltip {
margin: 0;
border: 0;
color: inherit;
overflow: visible;
transition-property: transform, scale, opacity, filter;
transition-duration: 200ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0);
filter: blur(8px);
}
&[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;
}
}
.media-minimal-skin .media-tooltip {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background-color: oklch(1 0 0 / 0.1);
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
box-shadow:
0 4px 6px -1px oklch(0 0 0 / 0.1),
0 2px 4px -2px oklch(0 0 0 / 0.1);
font-size: 0.75rem;
white-space: nowrap;
--media-tooltip-side-offset: 0.5rem;
@media (prefers-reduced-transparency: reduce) {
background-color: oklch(0 0 0 / 0.7);
}
@media (prefers-contrast: more) {
background-color: oklch(0 0 0 / 0.9);
}
}
/* ==========================================================================
Captions
========================================================================== */
.media-minimal-skin .media-captions {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 20;
font-size: 1rem;
text-wrap: balance;
pointer-events: none;
@container media-root (width > 20rem) {
font-size: 1.5rem;
}
@container media-root (width > 48rem) {
font-size: 1.875rem;
}
@container media-root (width > 80rem) {
font-size: 2.25rem;
}
}
.media-minimal-skin .media-captions__container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 42ch;
margin: 0 auto;
text-align: center;
}
.media-minimal-skin .media-captions__text {
display: block;
padding: 0.125rem 0.5rem;
color: oklch(1 0 0);
text-shadow:
0 0 1px oklch(0 0 0 / 0.7),
0 0 8px oklch(0 0 0 / 0.7);
text-align: center;
white-space: pre-wrap;
line-height: 1.2;
@media (prefers-contrast: more) {
background: oklch(0 0 0 / 0.7);
text-shadow: none;
box-decoration-break: clone;
}
& > * {
display: inline;
}
}
/* Caption shifting styles (custom and native) */
.media-minimal-skin {
--media-caption-track-delay: 600ms;
--media-caption-track-y: -0.5rem;
&:has(.media-controls[data-visible]) {
--media-caption-track-delay: 25ms;
--media-caption-track-y: -3rem;
}
}
.media-minimal-skin .media-captions,
.media-minimal-skin video::-webkit-media-text-track-container {
/* NOTE: The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: var(--media-caption-track-delay);
}
.media-minimal-skin video::-webkit-media-text-track-container {
transform: translateY(var(--media-caption-track-y)) scale(0.98);
z-index: 1;
font-family: inherit;
}
/* When controls are visible, shift captions up to avoid overlap */
.media-minimal-skin .media-controls[data-visible] ~ .media-captions {
transform: translateY(calc(var(--media-caption-track-y) - 0.5rem));
}
@media (prefers-reduced-motion: reduce) {
.media-minimal-skin .media-captions,
.media-minimal-skin video::-webkit-media-text-track-container {
transition-duration: 50ms;
}
}
/* ==========================================================================
Root
========================================================================== */
.media-minimal-skin--video {
background: oklch(0 0 0);
/* Border ring */
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
border-radius: inherit;
box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.15);
pointer-events: none;
@media (prefers-color-scheme: dark) {
box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.15);
}
}
/* Fullscreen */
&:fullscreen {
border-radius: 0;
}
}
/* ==========================================================================
Controls (hide/show behavior)
========================================================================== */
.media-minimal-skin--video .media-controls {
position: absolute;
bottom: 0;
inset-inline: 0;
z-index: 10;
gap: 0.5rem;
padding: 2rem 0.375rem 0.375rem 0.375rem;
will-change: transform, filter, opacity;
transition-property: transform, filter, opacity;
transition-duration: 75ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
color: oklch(1 0 0);
@container media-root (width > 40rem) {
gap: 0.875rem;
padding: 2.5rem 0.75rem 0.75rem 0.75rem;
}
&:not([data-visible]) {
opacity: 0;
transform: translateY(100%);
filter: blur(8px);
transition-duration: 500ms;
transition-delay: 500ms;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
scale: 1;
transform: translateY(0);
filter: blur(0);
transition-duration: 100ms;
}
}
}
/* ==========================================================================
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 {
--media-popover-side-offset: 0.5rem;
background: transparent;
padding: 0.25rem;
}
import { selectError } from '@videojs/core/dom';
import { type ComponentProps, forwardRef, type ReactNode, useRef, type PropsWithChildren, type CSSProperties } from 'react';
import { AlertDialog, Container, usePlayer, BufferingIndicator, CaptionsButton, Controls, FullscreenButton, MuteButton, PiPButton, PlayButton, PlaybackRateButton, Popover, SeekButton, Time, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
export interface ErrorDialogClasses {
root?: string;
dialog?: string;
content?: string;
title?: string;
description?: string;
actions?: string;
close?: string;
}
export function ErrorDialog({ classes }: { classes?: ErrorDialogClasses }): ReactNode {
const errorState = usePlayer(selectError);
const lastError = useRef(errorState?.error);
if (errorState?.error) lastError.current = errorState.error;
if (!errorState) return null;
return (
<AlertDialog.Root
open={!!errorState.error}
onOpenChange={(open) => {
if (!open) errorState.dismissError();
}}
>
<AlertDialog.Popup className={classes?.root}>
<div className={classes?.dialog}>
<div className={classes?.content}>
<AlertDialog.Title className={classes?.title}>Something went wrong.</AlertDialog.Title>
<AlertDialog.Description className={classes?.description}>
{lastError.current?.message ?? 'An error occurred while trying to play the video. Please try again.'}
</AlertDialog.Description>
</div>
<div className={classes?.actions}>
<AlertDialog.Close className={classes?.close}>OK</AlertDialog.Close>
</div>
</div>
</AlertDialog.Popup>
</AlertDialog.Root>
);
}
const SEEK_TIME = 10;
export type MinimalVideoSkinProps = PropsWithChildren<{ style?: CSSProperties; className?: string }>;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={['media-button', className].filter(Boolean).join(' ')} {...props} />;
});
const errorClasses = {
root: 'media-error',
dialog: 'media-error__dialog',
content: 'media-error__content',
title: 'media-error__title',
description: 'media-error__description',
actions: 'media-error__actions',
close: 'media-button',
};
function PlayLabel(): ReactNode {
const paused = usePlayer((s) => Boolean(s.paused));
const ended = usePlayer((s) => Boolean(s.ended));
if (ended) return <>Replay</>;
return paused ? <>Play</> : <>Pause</>;
}
function CaptionsLabel(): ReactNode {
const active = usePlayer((s) => Boolean(s.subtitlesShowing));
return active ? <>Disable captions</> : <>Enable captions</>;
}
function PiPLabel(): ReactNode {
const pip = usePlayer((s) => Boolean(s.pip));
return pip ? <>Exit picture-in-picture</> : <>Enter picture-in-picture</>;
}
function FullscreenLabel(): ReactNode {
const fullscreen = usePlayer((s) => Boolean(s.fullscreen));
return fullscreen ? <>Exit fullscreen</> : <>Enter fullscreen</>;
}
export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
const { children, className, ...rest } = props;
return (
<Container className={['media-minimal-skin media-minimal-skin--video', className].filter(Boolean).join(' ')} {...rest}>
{children}
<BufferingIndicator
render={(props) => (
<div {...props} className="media-buffering-indicator">
<svg className="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true" viewBox="0 0 18 18"><rect width="2" height="5" x="8" y=".5" opacity=".5" rx="1"><animate attributeName="opacity" begin="0s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="12.243" y="2.257" opacity=".45" rx="1" transform="rotate(45 13.243 4.757)"><animate attributeName="opacity" begin="0.125s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="12.5" y="8" opacity=".4" rx="1"><animate attributeName="opacity" begin="0.25s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="10.743" y="12.243" opacity=".35" rx="1" transform="rotate(45 13.243 13.243)"><animate attributeName="opacity" begin="0.375s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="8" y="12.5" opacity=".3" rx="1"><animate attributeName="opacity" begin="0.5s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="2" height="5" x="3.757" y="10.743" opacity=".25" rx="1" transform="rotate(45 4.757 13.243)"><animate attributeName="opacity" begin="0.625s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x=".5" y="8" opacity=".15" rx="1"><animate attributeName="opacity" begin="0.75s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect><rect width="5" height="2" x="2.257" y="3.757" opacity=".1" rx="1" transform="rotate(45 4.757 4.757)"><animate attributeName="opacity" begin="0.875s" calcMode="linear" dur="1s" repeatCount="indefinite" values="1;0"/></rect></svg>
</div>
)}
/>
<ErrorDialog classes={errorClasses} />
<Controls.Root className="media-controls">
<span className="media-button-group">
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--play">
<svg className="media-icon media-icon--restart" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M9 17a8 8 0 0 1-8-8h1.5a6.5 6.5 0 1 0 1.43-4.07l1.643 1.643A.25.25 0 0 1 5.396 7H1.25A.25.25 0 0 1 1 6.75V2.604a.25.25 0 0 1 .427-.177l1.438 1.438A8 8 0 1 1 9 17"/><path fill="currentColor" d="m11.61 9.639-3.331 2.07a.826.826 0 0 1-1.15-.266.86.86 0 0 1-.129-.452V6.849C7 6.38 7.374 6 7.834 6c.158 0 .312.045.445.13l3.331 2.071a.858.858 0 0 1 0 1.438"/></svg>
<svg className="media-icon media-icon--play" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="m13.473 10.476-6.845 4.256a1.697 1.697 0 0 1-2.364-.547 1.77 1.77 0 0 1-.264-.93v-8.51C4 3.78 4.768 3 5.714 3c.324 0 .64.093.914.268l6.845 4.255a1.763 1.763 0 0 1 0 2.953"/></svg>
<svg className="media-icon media-icon--pause" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="4" height="12" x="3" y="3" fill="currentColor" rx="1.75"/><rect width="4" height="12" x="11" y="3" fill="currentColor" rx="1.75"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-tooltip">
<PlayLabel />
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<svg className="media-icon media-icon--seek media-icon--flipped" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.06-1.06a6.5 6.5 0 1 1 9.665-8.665l-1.641 1.641a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<svg className="media-icon media-icon--seek" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M1 9c0 2.21.895 4.21 2.343 5.657l1.06-1.06a6.5 6.5 0 1 1 9.665-8.665l-1.641 1.641a.25.25 0 0 0 .177.427h4.146a.25.25 0 0 0 .25-.25V2.604a.25.25 0 0 0-.427-.177l-1.438 1.438A8 8 0 0 0 1 9"/></svg>
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
</Tooltip.Root>
</span>
<span className="media-time-controls">
<Time.Group className="media-time">
<Time.Value type="current" className="media-time__value media-time__value--current" />
<Time.Separator className="media-time__separator" />
<Time.Value type="duration" className="media-time__value media-time__value--duration" />
</Time.Group>
<TimeSlider.Root className="media-slider">
<TimeSlider.Track className="media-slider__track">
<TimeSlider.Fill className="media-slider__fill" />
<TimeSlider.Buffer className="media-slider__buffer" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider__thumb" />
</TimeSlider.Root>
</span>
<span className="media-button-group">
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button {...props} className="media-button--icon media-button--playback-rate" />}
/>
}
/>
<Tooltip.Popup className="media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<MuteButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--mute">
<svg className="media-icon media-icon--volume-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752M14.5 7.586l-1.768-1.768a1 1 0 1 0-1.414 1.414L13.085 9l-1.767 1.768a1 1 0 0 0 1.414 1.414l1.768-1.768 1.768 1.768a1 1 0 0 0 1.414-1.414L15.914 9l1.768-1.768a1 1 0 0 0-1.414-1.414z"/></svg>
<svg className="media-icon media-icon--volume-low" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
<svg className="media-icon media-icon--volume-high" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15.6 3.3c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4C15.4 5.9 16 7.4 16 9s-.6 3.1-1.8 4.3c-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7"/><path fill="currentColor" d="M.714 6.008h3.072l4.071-3.857c.5-.376 1.143 0 1.143.601V15.28c0 .602-.643.903-1.143.602l-4.071-3.858H.714c-.428 0-.714-.3-.714-.752V6.76c0-.451.286-.752.714-.752m10.568.59a.91.91 0 0 1 0-1.316.91.91 0 0 1 1.316 0c1.203 1.203 1.47 2.216 1.522 3.208q.012.255.011.51c0 1.16-.358 2.733-1.533 3.803a.7.7 0 0 1-.298.156c-.382.106-.873-.011-1.018-.156a.91.91 0 0 1 0-1.316c.57-.57.995-1.551.995-2.487 0-.944-.26-1.667-.995-2.402"/></svg>
</Button>
)}
/>
}
/>
<Popover.Popup className="media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--captions">
<svg className="media-icon media-icon--captions-off" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><rect width="16.5" height="12.5" x=".75" y="2.75" stroke="currentColor" strokeWidth="1.5" rx="3"/><rect width="3" height="1.5" x="3" y="8.5" fill="currentColor" rx=".75"/><rect width="2" height="1.5" x="13" y="8.5" fill="currentColor" rx=".75"/><rect width="4" height="1.5" x="11" y="11.5" fill="currentColor" rx=".75"/><rect width="5" height="1.5" x="7" y="8.5" fill="currentColor" rx=".75"/><rect width="7" height="1.5" x="3" y="11.5" fill="currentColor" rx=".75"/></svg>
<svg className="media-icon media-icon--captions-on" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15 2a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM3.75 11.5a.75.75 0 0 0 0 1.5h5.5a.75.75 0 0 0 0-1.5zm8 0a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5zm-8-3a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5zm4 0a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5zm6 0a.75.75 0 0 0 0 1.5h.5a.75.75 0 0 0 0-1.5z"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-tooltip">
<CaptionsLabel />
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton
render={(props) => (
<Button {...props} className="media-button--icon">
<svg className="media-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M13 2a4 4 0 0 1 4 4v2.645a3.5 3.5 0 0 0-1-.145h-.5V6A2.5 2.5 0 0 0 13 3.5H4A2.5 2.5 0 0 0 1.5 6v6A2.5 2.5 0 0 0 4 14.5h2.5v.5c0 .347.05.683.145 1H4a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4z"/><rect width="10" height="7" x="8" y="10" fill="currentColor" rx="2"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-tooltip">
<PiPLabel />
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--fullscreen">
<svg className="media-icon media-icon--fullscreen-enter" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M15.25 2a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0V3.5h-3.75a.75.75 0 0 1-.743-.648L10 2.75a.75.75 0 0 1 .75-.75z"/><path fill="currentColor" d="M14.72 2.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06zM2.75 10a.75.75 0 0 1 .75.75v3.75h3.75a.75.75 0 0 1 .743.648L8 15.25a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-4.5a.75.75 0 0 1 .75-.75"/><path fill="currentColor" d="M6.72 10.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06z"/></svg>
<svg className="media-icon media-icon--fullscreen-exit" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" aria-hidden="true" viewBox="0 0 18 18"><path fill="currentColor" d="M10.75 2a.75.75 0 0 1 .75.75V6.5h3.75a.75.75 0 0 1 .743.648L16 7.25a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-4.5a.75.75 0 0 1 .75-.75"/><path fill="currentColor" d="M14.72 2.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06zM7.25 10a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0V11.5H2.75a.75.75 0 0 1-.743-.648L2 10.75a.75.75 0 0 1 .75-.75z"/><path fill="currentColor" d="M6.72 10.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06z"/></svg>
</Button>
)}
/>
}
/>
<Tooltip.Popup className="media-tooltip">
<FullscreenLabel />
</Tooltip.Popup>
</Tooltip.Root>
</span>
</Controls.Root>
{/* <div className="media-captions">
<div className="media-captions__container">
<span className="media-captions__text">An example cue</span>
<span className="media-captions__text">
<p>Another example cue with HTML</p>
</span>
</div>
</div> */}
<div className="media-overlay" />
</Container>
);
}
/* ==========================================================================
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--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,
/* 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;
}
/* ==========================================================================
Tooltip Label State Visibility for Video Skins
Data-attribute-driven visibility rules for multi-state tooltip labels.
Uses adjacent sibling selectors to match button state → tooltip content.
========================================================================== */
/* --- All multi-state labels hidden by default --- */
.media-tooltip-label {
display: none;
}
/* --- Active label per state --- */
/* Play: ended → replay */
.media-button--play[data-ended] + .media-tooltip .media-tooltip-label--replay,
/* Play: paused (not ended) → play */
.media-button--play:not([data-ended])[data-paused] + .media-tooltip
.media-tooltip-label--play,
/* Play: playing (not paused, not ended) → pause */
.media-button--play:not([data-paused]):not([data-ended]) + .media-tooltip
.media-tooltip-label--pause,
/* Fullscreen: not fullscreen → enter */
.media-button--fullscreen:not([data-fullscreen]) + .media-tooltip
.media-tooltip-label--enter-fullscreen,
/* Fullscreen: fullscreen → exit */
.media-button--fullscreen[data-fullscreen] + .media-tooltip
.media-tooltip-label--exit-fullscreen,
/* Captions: not active → enable */
.media-button--captions:not([data-active]) + .media-tooltip
.media-tooltip-label--enable-captions,
/* Captions: active → disable */
.media-button--captions[data-active] + .media-tooltip
.media-tooltip-label--disable-captions,
/* PiP: not in pip → enter */
.media-button--pip:not([data-pip]) + .media-tooltip
.media-tooltip-label--enter-pip,
/* PiP: in pip → exit */
.media-button--pip[data-pip] + .media-tooltip
.media-tooltip-label--exit-pip {
display: block;
}
/* ==========================================================================
Reset
========================================================================== */
.media-minimal-skin *,
.media-minimal-skin *::before,
.media-minimal-skin *::after {
box-sizing: border-box;
margin: 0;
}
.media-minimal-skin img,
.media-minimal-skin video,
.media-minimal-skin svg {
display: block;
max-width: 100%;
}
.media-minimal-skin button {
font: inherit;
}
@media (prefers-reduced-motion: no-preference) {
.media-minimal-skin {
interpolate-size: allow-keywords;
}
}
/* ==========================================================================
Root Container
========================================================================== */
.media-minimal-skin {
position: relative;
isolation: isolate;
display: block;
container: media-root / inline-size;
border-radius: var(--media-border-radius, 0.75rem);
font-family:
Inter Variable,
Inter,
ui-sans-serif,
system-ui,
sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
letter-spacing: normal;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
/* ==========================================================================
Media Element
========================================================================== */
.media-minimal-skin ::slotted(video),
.media-minimal-skin video {
display: block;
width: 100%;
height: 100%;
border-radius: var(--media-border-radius, 0.75rem);
}
/* ==========================================================================
Poster Image
========================================================================== */
.media-minimal-skin > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
transition: opacity 0.25s;
pointer-events: none;
&:not([data-visible]) {
opacity: 0;
}
}
/* ==========================================================================
Overlay / Scrim
========================================================================== */
.media-minimal-skin .media-overlay {
position: absolute;
inset: 0;
border-radius: inherit;
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));
backdrop-filter: blur(0) saturate(1.2) brightness(0.9);
opacity: 0;
transition-property: opacity, backdrop-filter;
transition-duration: 500ms;
transition-delay: 500ms;
transition-timing-function: ease-out;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
}
}
.media-minimal-skin .media-controls[data-visible] ~ .media-overlay,
.media-minimal-skin .media-error[data-open] ~ .media-overlay {
opacity: 1;
transition-duration: 150ms;
transition-delay: 0ms;
}
.media-minimal-skin .media-error[data-open] ~ .media-overlay {
backdrop-filter: blur(8px) saturate(1.2) brightness(0.9);
}
/* ==========================================================================
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;
&[data-visible] {
display: flex;
}
}
/* ==========================================================================
Error Dialog
========================================================================== */
.media-minimal-skin .media-error {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.media-minimal-skin .media-error__dialog {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 16rem;
padding: 1rem;
color: oklch(1 0 0);
font-size: 0.875rem;
text-shadow: 0 1px 0 oklch(0 0 0 / 0.5);
transition-property: opacity, transform;
transition-duration: 500ms;
transition-delay: 100ms;
transition-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
);
/* Simple, fast transition for reduced motion users */
@media (prefers-reduced-motion: reduce) {
transition-duration: 100ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
}
}
.media-minimal-skin .media-error[data-starting-style] .media-error__dialog,
.media-minimal-skin .media-error[data-ending-style] .media-error__dialog {
opacity: 0;
transform: scale(0.5);
}
.media-minimal-skin .media-error__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.375rem 0;
}
.media-minimal-skin .media-error__title {
font-weight: 600;
line-height: 1.25;
}
.media-minimal-skin .media-error__description {
opacity: 0.7;
}
.media-minimal-skin .media-error__actions {
display: flex;
gap: 0.5rem;
& > * {
flex: 1;
}
}
/* ==========================================================================
Controls
========================================================================== */
.media-minimal-skin .media-controls {
container: media-controls / inline-size;
display: flex;
align-items: center;
--media-controls-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.25));
--media-controls-current-shadow-color-subtle: oklch(
from var(--media-controls-current-shadow-color) l c h /
calc(alpha * 0.4)
);
text-shadow: 0 0 1px var(--media-controls-current-shadow-color);
}
/* ==========================================================================
Time Controls & Display
========================================================================== */
.media-minimal-skin .media-time-controls {
display: flex;
flex-direction: row-reverse;
align-items: center;
flex: 1;
gap: 0.75rem;
}
.media-minimal-skin .media-time {
display: flex;
align-items: center;
gap: 0.25rem;
}
.media-minimal-skin .media-time__value {
font-variant-numeric: tabular-nums;
}
.media-minimal-skin .media-time__value--current,
.media-minimal-skin .media-time__separator {
display: none;
}
@container media-controls (width > 28rem) {
.media-minimal-skin .media-time-controls {
flex-direction: row;
}
.media-minimal-skin .media-time__value--duration,
.media-minimal-skin .media-time__separator {
color: oklch(from currentColor l c h / 0.6);
}
.media-minimal-skin .media-time__value--current,
.media-minimal-skin .media-time__separator {
display: inline;
}
}
/* ==========================================================================
Button Groups
========================================================================== */
.media-minimal-skin .media-button-group {
display: flex;
align-items: center;
gap: 0.075rem;
@container media-root (width > 40rem) {
gap: 0.125rem;
}
}
/* ==========================================================================
Buttons
========================================================================== */
/* Base button */
.media-minimal-skin .media-button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0.5rem 1rem;
background: oklch(1 0 0);
border: none;
border-radius: 0.5rem;
outline: 2px solid transparent;
outline-offset: -2px;
color: oklch(0 0 0);
font-weight: 500;
text-align: center;
text-shadow: inherit;
transition-property: background-color, color, outline-offset, transform;
transition-duration: 150ms;
transition-timing-function: ease-out;
cursor: pointer;
user-select: none;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
&[disabled] {
opacity: 0.5;
filter: grayscale(1);
cursor: not-allowed;
}
&[data-availability="unavailable"] {
display: none;
}
}
/* Icon button variant */
.media-minimal-skin .media-button--icon {
display: grid;
width: 2.375rem;
padding: 0;
aspect-ratio: 1;
background: transparent;
color: inherit;
&:hover,
&:focus-visible,
&[aria-expanded="true"] {
color: oklch(from currentColor l c h / 0.8);
text-decoration: none;
}
&:active {
transform: scale(0.9);
}
& .media-icon {
filter: drop-shadow(0 1px 0 var(--media-controls-current-shadow-color, oklch(0 0 0 / 0.25)));
}
}
/* Seek button */
.media-minimal-skin .media-button--seek {
& .media-icon__label {
position: absolute;
right: -1px;
bottom: -3px;
font-size: 0.75em;
font-weight: 480;
font-variant-numeric: tabular-nums;
}
&:has(.media-icon--flipped) .media-icon__label {
right: unset;
left: -1px;
}
@container media-controls (width < 28rem) {
display: none;
}
}
/* Playback rate button */
.media-minimal-skin .media-button--playback-rate {
padding: 0;
&::after {
content: attr(data-rate) "\00D7";
width: 4ch;
font-variant-numeric: tabular-nums;
}
}
/* ==========================================================================
Icons
========================================================================== */
.media-minimal-skin .media-icon__container {
position: relative;
}
.media-minimal-skin .media-icon {
display: block;
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
.media-minimal-skin .media-icon--flipped {
scale: -1 1;
}
/* ==========================================================================
Slider
========================================================================== */
.media-minimal-skin .media-slider {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
border-radius: calc(infinity * 1px);
outline: none;
&[data-orientation="horizontal"] {
min-width: 5rem;
width: 100%;
height: 1.25rem;
}
&[data-orientation="vertical"] {
width: 1.25rem;
height: 4.5rem;
}
}
/* Track */
.media-minimal-skin .media-slider__track {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: inherit;
user-select: none;
background-color: oklch(from currentColor l c h / 0.2);
&[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;
transform: translate(-50%, -50%);
z-index: 10;
width: 0.75rem;
height: 0.75rem;
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-controls-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;
scale: 0.7;
transform-origin: center;
transition-property: opacity, scale, outline-offset;
transition-duration: 150ms;
transition-timing-function: ease-out;
user-select: none;
outline: 2px solid transparent;
outline-offset: -2px;
&[data-orientation="horizontal"] {
top: 50%;
left: var(--media-slider-fill);
}
&[data-orientation="vertical"] {
left: 50%;
top: calc(100% - var(--media-slider-fill));
}
&: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;
border-radius: inherit;
pointer-events: none;
}
.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-duration: 0.25s;
transition-timing-function: ease-out;
&[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);
}
}
/* Time display within slider */
.media-minimal-skin .media-slider__time-display {
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Popups & Animations
========================================================================== */
.media-minimal-skin .media-popover,
.media-minimal-skin .media-tooltip {
margin: 0;
border: 0;
color: inherit;
overflow: visible;
transition-property: transform, scale, opacity, filter;
transition-duration: 200ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0);
filter: blur(8px);
}
&[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;
}
}
.media-minimal-skin .media-tooltip {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background-color: oklch(1 0 0 / 0.1);
backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
box-shadow:
0 4px 6px -1px oklch(0 0 0 / 0.1),
0 2px 4px -2px oklch(0 0 0 / 0.1);
font-size: 0.75rem;
white-space: nowrap;
--media-tooltip-side-offset: 0.5rem;
@media (prefers-reduced-transparency: reduce) {
background-color: oklch(0 0 0 / 0.7);
}
@media (prefers-contrast: more) {
background-color: oklch(0 0 0 / 0.9);
}
}
/* ==========================================================================
Captions
========================================================================== */
.media-minimal-skin .media-captions {
position: absolute;
inset: auto 1rem 1.5rem 1rem;
z-index: 20;
font-size: 1rem;
text-wrap: balance;
pointer-events: none;
@container media-root (width > 20rem) {
font-size: 1.5rem;
}
@container media-root (width > 48rem) {
font-size: 1.875rem;
}
@container media-root (width > 80rem) {
font-size: 2.25rem;
}
}
.media-minimal-skin .media-captions__container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 42ch;
margin: 0 auto;
text-align: center;
}
.media-minimal-skin .media-captions__text {
display: block;
padding: 0.125rem 0.5rem;
color: oklch(1 0 0);
text-shadow:
0 0 1px oklch(0 0 0 / 0.7),
0 0 8px oklch(0 0 0 / 0.7);
text-align: center;
white-space: pre-wrap;
line-height: 1.2;
@media (prefers-contrast: more) {
background: oklch(0 0 0 / 0.7);
text-shadow: none;
box-decoration-break: clone;
}
& > * {
display: inline;
}
}
/* Caption shifting styles (custom and native) */
.media-minimal-skin {
--media-caption-track-delay: 600ms;
--media-caption-track-y: -0.5rem;
&:has(.media-controls[data-visible]) {
--media-caption-track-delay: 25ms;
--media-caption-track-y: -3rem;
}
}
.media-minimal-skin .media-captions,
.media-minimal-skin video::-webkit-media-text-track-container {
/* NOTE: The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: var(--media-caption-track-delay);
}
.media-minimal-skin video::-webkit-media-text-track-container {
transform: translateY(var(--media-caption-track-y)) scale(0.98);
z-index: 1;
font-family: inherit;
}
/* When controls are visible, shift captions up to avoid overlap */
.media-minimal-skin .media-controls[data-visible] ~ .media-captions {
transform: translateY(calc(var(--media-caption-track-y) - 0.5rem));
}
@media (prefers-reduced-motion: reduce) {
.media-minimal-skin .media-captions,
.media-minimal-skin video::-webkit-media-text-track-container {
transition-duration: 50ms;
}
}
/* ==========================================================================
Root
========================================================================== */
.media-minimal-skin--video {
background: oklch(0 0 0);
/* Border ring */
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
border-radius: inherit;
box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.15);
pointer-events: none;
@media (prefers-color-scheme: dark) {
box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.15);
}
}
/* Fullscreen */
&:fullscreen {
border-radius: 0;
}
}
/* ==========================================================================
Controls (hide/show behavior)
========================================================================== */
.media-minimal-skin--video .media-controls {
position: absolute;
bottom: 0;
inset-inline: 0;
z-index: 10;
gap: 0.5rem;
padding: 2rem 0.375rem 0.375rem 0.375rem;
will-change: transform, filter, opacity;
transition-property: transform, filter, opacity;
transition-duration: 75ms;
transition-delay: 0ms;
transition-timing-function: ease-out;
color: oklch(1 0 0);
@container media-root (width > 40rem) {
gap: 0.875rem;
padding: 2.5rem 0.75rem 0.75rem 0.75rem;
}
&:not([data-visible]) {
opacity: 0;
transform: translateY(100%);
filter: blur(8px);
transition-duration: 500ms;
transition-delay: 500ms;
pointer-events: none;
@media (prefers-reduced-motion: reduce) {
scale: 1;
transform: translateY(0);
filter: blur(0);
transition-duration: 100ms;
}
}
}
/* ==========================================================================
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 {
--media-popover-side-offset: 0.5rem;
background: transparent;
padding: 0.25rem;
}
v10 is built different
Video.js 10 is complete ground-up rewrite of the player for the modern web.
The UI is separated from the underlying media renderer. Every component is
independent and works together through open API contracts. We've structured
the project for modern JavaScript bundlers to support tree-shaking and
intelligent code splitting.
Start with a small player and only add what you need.
Download speeds
(based on slow 4G connection)VJS 8
VJS 8
3.96s / 200kB
VJS 10
VJS 10
0.75s / 38kB
Roadmap
Oct 2025
Tech Preview
Kick the tires, and light the fires
Mar 2026
Beta
Close to stable. Experimental adoption in real projects
Mid 2026
GA
Stable APIs. Feature parity w/ Media Chrome, Vidstack, Plyr.
End of 2026
Core Feature Parity
Video.js core/contrib plugins migrated
Sponsors
Corporate Shepherd
The role of Corporate Shepherd is held by the company that has been elected by the Video.js Technical Steering Committee (TSC) to be a steward of the project and make significant investment into the development of Video.js. The role is currently held by Mux, taking over for Brigthcove in 2025. Mux is a leader in streaming video technology and was founded by Steve Heffernan, the creator of Video.js.