diff --git a/examples/html-eject-frosted/index.html b/examples/html-eject-frosted/index.html
new file mode 100644
index 00000000..58968598
--- /dev/null
+++ b/examples/html-eject-frosted/index.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+ HTML Frosted Ejected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/html-eject-frosted/package.json b/examples/html-eject-frosted/package.json
new file mode 100644
index 00000000..a1e2bf54
--- /dev/null
+++ b/examples/html-eject-frosted/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "html-eject-frosted",
+ "type": "module",
+ "version": "1.0.0",
+ "private": true,
+ "description": "HTML example for Video.js 10",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@videojs/html": "workspace:*"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.2",
+ "vite": "^7.1.6"
+ }
+}
diff --git a/examples/html-eject-frosted/src/frosted.css b/examples/html-eject-frosted/src/frosted.css
new file mode 100644
index 00000000..0f60fbc0
--- /dev/null
+++ b/examples/html-eject-frosted/src/frosted.css
@@ -0,0 +1,360 @@
+media-container * {
+ box-sizing: border-box;
+}
+
+media-container {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+media-container::before,
+media-container::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+}
+media-container::before {
+ inset: 1px;
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
+}
+media-container::after {
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
+}
+
+/* Fullscreen */
+media-container:fullscreen {
+ border-radius: 0;
+}
+
+media-container > ::slotted([slot='media']),
+media-container > video,
+media-container > audio {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+media-container > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
+ backdrop-filter: saturate(1.5) brightness(0.9);
+ transition: opacity 0.15s ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+media-container:hover > .overlay,
+media-container:has([data-paused]) > .overlay,
+media-container:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Common Surface Styles - e.g. tooltips, popovers, controls */
+.surface {
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ inset 0 0 0 1px oklab(1 0 0 / 0.15),
+ 0 0 0 1px oklab(0 0 0 / 0.15),
+ oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
+ oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
+}
+@media (prefers-reduced-transparency: reduce) {
+ .surface {
+ background-color: oklab(0 0 0 / 0.7);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+@media (prefers-contrast: more) {
+ .surface {
+ background-color: oklab(0 0 0 / 0.9);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Media Control Bar UI/Styles */
+.control-bar {
+ position: absolute;
+ bottom: 0.75rem;
+ inset-inline: 0.75rem;
+ padding: 0.25rem;
+ display: flex;
+ align-items: center;
+ gap: 0.125rem;
+ border-radius: calc(infinity * 1px);
+ will-change: scale, transform, filter, opacity;
+ scale: 0.9;
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: scale, transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ transform-origin: bottom;
+ color: oklab(1 0 0);
+}
+media-container:hover > .control-bar,
+media-container:has([data-paused]) > .control-bar,
+media-container:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ scale: 1;
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.time-controls {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 0.75rem;
+ padding-inline: 0.375rem;
+}
+media-current-time-display,
+media-duration-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
+ font-variant-numeric: tabular-nums;
+}
+
+/* Generic Media Button Styling */
+.button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.5rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: calc(infinity * 1px);
+ color: oklab(1 0 0 / 0.9);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.button:hover,
+.button:focus-visible,
+.button[aria-expanded='true'] {
+ background-color: oklab(1 0 0 / 0.1);
+ color: oklab(1 0 0);
+ text-decoration: none;
+}
+.button:focus-visible {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 2px;
+}
+.button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
+}
+
+/* Media Play Button UI/Styles */
+media-play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+media-play-button:not([data-paused]) .pause-icon,
+media-play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+media-fullscreen-button .icon {
+ display: none;
+}
+media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+media-mute-button .icon {
+ display: none;
+}
+media-mute-button:not([data-volume-level]) .volume-low-icon,
+media-mute-button[data-volume-level='high'] .volume-high-icon,
+media-mute-button[data-volume-level='low'] .volume-low-icon,
+media-mute-button[data-volume-level='medium'] .volume-low-icon,
+media-mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.slider[data-orientation='vertical'] {
+ height: 5rem;
+ width: 1.25rem;
+}
+
+.slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.25rem;
+}
+
+/* Vertical track styles */
+.slider-track[data-orientation='vertical'] {
+ width: 0.25rem;
+ height: 100%;
+}
+
+.slider:focus-visible .slider-track {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 6px;
+}
+
+.slider-thumb {
+ width: 0.625rem;
+ height: 0.625rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ transition-property: opacity, height, width;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.slider-thumb:active {
+ width: 0.75rem;
+ height: 0.75rem;
+}
+.slider:hover .slider-thumb,
+.slider:focus-within .slider-thumb {
+ opacity: 1;
+}
+.slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.slider-pointer {
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+}
+
+.slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.popup-animation[data-starting-style],
+.popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+media-popover {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+media-tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.625rem;
+ border-radius: calc(infinity * 1px);
+ font-size: 0.75rem;
+}
+
+.tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+media-tooltip-popup[data-paused] .play-tooltip,
+media-tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}
diff --git a/examples/html-eject-frosted/src/main.ts b/examples/html-eject-frosted/src/main.ts
new file mode 100644
index 00000000..c9bf17ae
--- /dev/null
+++ b/examples/html-eject-frosted/src/main.ts
@@ -0,0 +1,14 @@
+import './frosted.css';
+import '@videojs/html/icons';
+import '@videojs/html/define/media-container';
+import '@videojs/html/define/media-provider';
+import '@videojs/html/define/media-play-button';
+import '@videojs/html/define/media-mute-button';
+import '@videojs/html/define/media-volume-slider';
+import '@videojs/html/define/media-time-slider';
+import '@videojs/html/define/media-fullscreen-button';
+import '@videojs/html/define/media-duration-display';
+import '@videojs/html/define/media-current-time-display';
+import '@videojs/html/define/media-preview-time-display';
+import '@videojs/html/define/media-popover';
+import '@videojs/html/define/media-tooltip';
diff --git a/examples/html-eject-frosted/tsconfig.json b/examples/html-eject-frosted/tsconfig.json
new file mode 100644
index 00000000..50b8061b
--- /dev/null
+++ b/examples/html-eject-frosted/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "."
+ },
+ "include": ["src"]
+}
diff --git a/examples/html-eject-frosted/vite.config.ts b/examples/html-eject-frosted/vite.config.ts
new file mode 100644
index 00000000..c9d7dbf3
--- /dev/null
+++ b/examples/html-eject-frosted/vite.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite';
+
+// https://vitejs.dev/config
+export default defineConfig({
+ server: {
+ port: 5174,
+ },
+ optimizeDeps: {
+ exclude: ['@vjs/html'],
+ },
+});
diff --git a/examples/html-eject-minimal/index.html b/examples/html-eject-minimal/index.html
new file mode 100644
index 00000000..c004adea
--- /dev/null
+++ b/examples/html-eject-minimal/index.html
@@ -0,0 +1,150 @@
+
+
+
+
+
+ HTML Minimal Ejected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/html-eject-minimal/package.json b/examples/html-eject-minimal/package.json
new file mode 100644
index 00000000..e3aea64b
--- /dev/null
+++ b/examples/html-eject-minimal/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "html-eject-minimal",
+ "type": "module",
+ "version": "1.0.0",
+ "private": true,
+ "description": "HTML example for Video.js 10",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@videojs/html": "workspace:*"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.2",
+ "vite": "^7.1.6"
+ }
+}
diff --git a/examples/html-eject-minimal/src/main.ts b/examples/html-eject-minimal/src/main.ts
new file mode 100644
index 00000000..f1790dc9
--- /dev/null
+++ b/examples/html-eject-minimal/src/main.ts
@@ -0,0 +1,14 @@
+import './minimal.css';
+import '@videojs/html/icons';
+import '@videojs/html/define/media-container';
+import '@videojs/html/define/media-provider';
+import '@videojs/html/define/media-play-button';
+import '@videojs/html/define/media-mute-button';
+import '@videojs/html/define/media-volume-slider';
+import '@videojs/html/define/media-time-slider';
+import '@videojs/html/define/media-fullscreen-button';
+import '@videojs/html/define/media-duration-display';
+import '@videojs/html/define/media-current-time-display';
+import '@videojs/html/define/media-preview-time-display';
+import '@videojs/html/define/media-popover';
+import '@videojs/html/define/media-tooltip';
diff --git a/examples/html-eject-minimal/src/minimal.css b/examples/html-eject-minimal/src/minimal.css
new file mode 100644
index 00000000..2d774ff2
--- /dev/null
+++ b/examples/html-eject-minimal/src/minimal.css
@@ -0,0 +1,352 @@
+media-container * {
+ box-sizing: border-box;
+}
+
+media-container {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+media-container::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
+}
+@media (prefers-color-scheme: dark) {
+ media-container::after {
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Fullscreen */
+media-container:fullscreen {
+ border-radius: 0;
+}
+
+media-container > ::slotted([slot='media']),
+media-container > video,
+media-container > audio {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+media-container > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
+ transform: translateY(100%);
+ transition-property: transform, opacity;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+media-container:hover > .overlay,
+media-container:has([data-paused]) > .overlay,
+media-container:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transform: translateY(0);
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Media Control Bar UI/Styles */
+.control-bar {
+ position: absolute;
+ bottom: 0;
+ inset-inline: 0;
+ padding: 2.5rem 0.75rem 0.75rem 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.875rem;
+ will-change: transform, filter, opacity;
+ transform: translateY(100%);
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ color: oklab(1 0 0);
+}
+media-container:hover > .control-bar,
+media-container:has([data-paused]) > .control-bar,
+media-container:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.time-display-group {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+.duration-display {
+ display: contents;
+ color: oklab(1 0 0 / 0.5);
+}
+media-current-time-display,
+media-duration-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
+ font-variant-numeric: tabular-nums;
+}
+
+.button-group {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+/* Generic Media Button Styling */
+.button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.625rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: 0.375rem;
+ color: oklab(1 0 0);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.button:hover,
+.button:focus-visible,
+.button[aria-expanded='true'] {
+ color: oklab(1 0 0 / 0.8);
+ text-decoration: none;
+}
+.button:focus-visible {
+ outline-color: oklab(1 0 0);
+ outline-offset: 2px;
+}
+.button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
+}
+
+/* Media Play Button UI/Styles */
+media-play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+media-play-button:not([data-paused]) .pause-icon,
+media-play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+media-fullscreen-button .icon {
+ display: none;
+}
+media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+media-mute-button .icon {
+ display: none;
+}
+media-mute-button:not([data-volume-level]) .volume-low-icon,
+media-mute-button[data-volume-level='high'] .volume-high-icon,
+media-mute-button[data-volume-level='low'] .volume-low-icon,
+media-mute-button[data-volume-level='medium'] .volume-low-icon,
+media-mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.slider[data-orientation='vertical'] {
+ height: 4.5rem;
+ width: 1.25rem;
+}
+
+.slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.1875rem;
+}
+
+/* Vertical track styles */
+.slider-track[data-orientation='vertical'] {
+ width: 0.1875rem;
+ height: 100%;
+}
+
+.slider:focus-visible .slider-track {
+ outline-color: oklab(1 0 0);
+ outline-offset: 6px;
+}
+
+.slider-thumb {
+ width: 0.75rem;
+ height: 0.75rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ scale: 0.7;
+ transform-origin: center;
+ transition-property: opacity, scale;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.slider:hover .slider-thumb,
+.slider:focus-within .slider-thumb {
+ opacity: 1;
+ scale: 1;
+}
+.slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.slider-pointer {
+ display: none;
+}
+
+.slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.popup-animation[data-starting-style],
+.popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+media-popover {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+media-tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ 0 4px 6px -1px oklab(0 0 0 / 0.1),
+ 0 2px 4px -2px oklab(0 0 0 / 0.1);
+}
+@media (prefers-reduced-transparency: reduce) {
+ media-tooltip-popup {
+ background-color: oklab(0 0 0 / 0.7);
+ }
+}
+@media (prefers-contrast: more) {
+ media-tooltip-popup {
+ background-color: oklab(0 0 0 / 0.9);
+ }
+}
+
+.tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+media-tooltip-popup[data-paused] .play-tooltip,
+media-tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}
diff --git a/examples/html-eject-minimal/tsconfig.json b/examples/html-eject-minimal/tsconfig.json
new file mode 100644
index 00000000..50b8061b
--- /dev/null
+++ b/examples/html-eject-minimal/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": "."
+ },
+ "include": ["src"]
+}
diff --git a/examples/html-eject-minimal/vite.config.ts b/examples/html-eject-minimal/vite.config.ts
new file mode 100644
index 00000000..c9d7dbf3
--- /dev/null
+++ b/examples/html-eject-minimal/vite.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite';
+
+// https://vitejs.dev/config
+export default defineConfig({
+ server: {
+ port: 5174,
+ },
+ optimizeDeps: {
+ exclude: ['@vjs/html'],
+ },
+});
diff --git a/examples/react-eject-frosted/index.html b/examples/react-eject-frosted/index.html
new file mode 100644
index 00000000..32220ada
--- /dev/null
+++ b/examples/react-eject-frosted/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+ React Frosted Ejected
+
+
+
+
+
+
+
+
+
diff --git a/examples/react-eject-frosted/package.json b/examples/react-eject-frosted/package.json
new file mode 100644
index 00000000..1cd669f9
--- /dev/null
+++ b/examples/react-eject-frosted/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "react-eject-frosted",
+ "type": "module",
+ "version": "1.0.0",
+ "private": true,
+ "description": "React example for Video.js 10",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@videojs/react": "workspace:*",
+ "clsx": "^2.1.1",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "screenfull": "^6.0.2",
+ "tailwindcss": "^4.1.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.0",
+ "@types/react": "^18.0.0",
+ "@types/react-dom": "^18.0.0",
+ "@vitejs/plugin-react": "^5.0.3",
+ "typescript": "^5.9.2",
+ "vite": "^7.1.6"
+ }
+}
diff --git a/examples/react-eject-frosted/src/FrostedSkin.tsx b/examples/react-eject-frosted/src/FrostedSkin.tsx
new file mode 100644
index 00000000..9a87eb4c
--- /dev/null
+++ b/examples/react-eject-frosted/src/FrostedSkin.tsx
@@ -0,0 +1,115 @@
+import type { PropsWithChildren } from 'react';
+
+import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
+import {
+ FullscreenEnterIcon,
+ FullscreenExitIcon,
+ PauseIcon,
+ PlayIcon,
+ VolumeHighIcon,
+ VolumeLowIcon,
+ VolumeOffIcon,
+} from '@videojs/react/icons';
+
+import './frosted.css';
+
+type SkinProps = PropsWithChildren<{
+ className?: string;
+}>;
+
+export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element {
+ return (
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Play
+ Pause
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter Fullscreen
+ Exit Fullscreen
+
+
+
+
+
+
+ );
+}
diff --git a/examples/react-eject-frosted/src/frosted.css b/examples/react-eject-frosted/src/frosted.css
new file mode 100644
index 00000000..6f018520
--- /dev/null
+++ b/examples/react-eject-frosted/src/frosted.css
@@ -0,0 +1,357 @@
+.vjs-frosted-skin * {
+ box-sizing: border-box;
+}
+
+.vjs-frosted-skin {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+.vjs-frosted-skin::before,
+.vjs-frosted-skin::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+}
+.vjs-frosted-skin::before {
+ inset: 1px;
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
+}
+.vjs-frosted-skin::after {
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
+}
+
+/* Fullscreen */
+.vjs-frosted-skin:fullscreen {
+ border-radius: 0;
+}
+
+.vjs-frosted-skin > ::slotted([slot='media']) {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+.vjs-frosted-skin > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
+ backdrop-filter: saturate(1.5) brightness(0.9);
+ transition: opacity 0.15s ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+.vjs-frosted-skin:hover > .overlay,
+.vjs-frosted-skin:has([data-paused]) > .overlay,
+.vjs-frosted-skin:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Common Surface Styles - e.g. tooltips, popovers, controls */
+.vjs-frosted-skin .surface {
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ inset 0 0 0 1px oklab(1 0 0 / 0.15),
+ 0 0 0 1px oklab(0 0 0 / 0.15),
+ oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
+ oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
+}
+@media (prefers-reduced-transparency: reduce) {
+ .vjs-frosted-skin .surface {
+ background-color: oklab(0 0 0 / 0.7);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+@media (prefers-contrast: more) {
+ .vjs-frosted-skin .surface {
+ background-color: oklab(0 0 0 / 0.9);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Media Control Bar UI/Styles */
+.vjs-frosted-skin .control-bar {
+ position: absolute;
+ bottom: 0.75rem;
+ inset-inline: 0.75rem;
+ padding: 0.25rem;
+ display: flex;
+ align-items: center;
+ gap: 0.125rem;
+ border-radius: calc(infinity * 1px);
+ will-change: scale, transform, filter, opacity;
+ scale: 0.9;
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: scale, transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ transform-origin: bottom;
+ color: oklab(1 0 0);
+}
+.vjs-frosted-skin:hover > .control-bar,
+.vjs-frosted-skin:has([data-paused]) > .control-bar,
+.vjs-frosted-skin:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ scale: 1;
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.vjs-frosted-skin .time-controls {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 0.75rem;
+ padding-inline: 0.375rem;
+}
+.vjs-frosted-skin .time-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
+ font-variant-numeric: tabular-nums;
+}
+
+/* Generic Media Button Styling */
+.vjs-frosted-skin .button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.5rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: calc(infinity * 1px);
+ color: oklab(1 0 0 / 0.9);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-frosted-skin .button:hover,
+.vjs-frosted-skin .button:focus-visible,
+.vjs-frosted-skin .button[aria-expanded='true'] {
+ background-color: oklab(1 0 0 / 0.1);
+ color: oklab(1 0 0);
+ text-decoration: none;
+}
+.vjs-frosted-skin .button:focus-visible {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 2px;
+}
+.vjs-frosted-skin .button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.vjs-frosted-skin .button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
+}
+
+/* Media Play Button UI/Styles */
+.vjs-frosted-skin .play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+.vjs-frosted-skin .play-button:not([data-paused]) .pause-icon,
+.vjs-frosted-skin .play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+.vjs-frosted-skin .fullscreen-button .icon {
+ display: none;
+}
+.vjs-frosted-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+.vjs-frosted-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+.vjs-frosted-skin .mute-button .icon {
+ display: none;
+}
+.vjs-frosted-skin .mute-button:not([data-volume-level]) .volume-low-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='high'] .volume-high-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='low'] .volume-low-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='medium'] .volume-low-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.vjs-frosted-skin .slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.vjs-frosted-skin .slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.vjs-frosted-skin .slider[data-orientation='vertical'] {
+ height: 5rem;
+ width: 1.25rem;
+}
+
+.vjs-frosted-skin .slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.vjs-frosted-skin .slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.25rem;
+}
+
+/* Vertical track styles */
+.vjs-frosted-skin .slider-track[data-orientation='vertical'] {
+ width: 0.25rem;
+ height: 100%;
+}
+
+.vjs-frosted-skin .slider:focus-visible .slider-track {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 6px;
+}
+
+.vjs-frosted-skin .slider-thumb {
+ width: 0.625rem;
+ height: 0.625rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ transition-property: opacity, height, width;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-frosted-skin .slider-thumb:active {
+ width: 0.75rem;
+ height: 0.75rem;
+}
+.vjs-frosted-skin .slider:hover .slider-thumb,
+.vjs-frosted-skin .slider:focus-within .slider-thumb {
+ opacity: 1;
+}
+.vjs-frosted-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.vjs-frosted-skin .slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.vjs-frosted-skin .slider-pointer {
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+}
+
+.vjs-frosted-skin .slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+.vjs-frosted-skin .media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.vjs-frosted-skin .popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.vjs-frosted-skin .popup-animation[data-starting-style],
+.vjs-frosted-skin .popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.vjs-frosted-skin .popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+.vjs-frosted-skin .popover-popup {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+.vjs-frosted-skin .tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.625rem;
+ border-radius: calc(infinity * 1px);
+ font-size: 0.75rem;
+}
+
+.vjs-frosted-skin .tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+.vjs-frosted-skin .tooltip-popup[data-paused] .play-tooltip,
+.vjs-frosted-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+.vjs-frosted-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+.vjs-frosted-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}
diff --git a/examples/react-eject-frosted/src/main.tsx b/examples/react-eject-frosted/src/main.tsx
new file mode 100644
index 00000000..61f69887
--- /dev/null
+++ b/examples/react-eject-frosted/src/main.tsx
@@ -0,0 +1,20 @@
+import { MediaProvider, Video } from '@videojs/react';
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import FrostedSkin from './FrostedSkin';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+ ,
+);
diff --git a/examples/react-eject-frosted/tsconfig.json b/examples/react-eject-frosted/tsconfig.json
new file mode 100644
index 00000000..40aa8ebc
--- /dev/null
+++ b/examples/react-eject-frosted/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "baseUrl": "."
+ },
+ "include": ["src"]
+}
diff --git a/examples/react-eject-frosted/vite.config.ts b/examples/react-eject-frosted/vite.config.ts
new file mode 100644
index 00000000..6869bb0a
--- /dev/null
+++ b/examples/react-eject-frosted/vite.config.ts
@@ -0,0 +1,15 @@
+import tailwindcss from '@tailwindcss/vite';
+
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
+// https://vitejs.dev/config
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ server: {
+ port: 5173,
+ },
+ optimizeDeps: {
+ exclude: ['@videojs/react'],
+ },
+});
diff --git a/examples/react-eject-minimal/index.html b/examples/react-eject-minimal/index.html
new file mode 100644
index 00000000..497b4a8a
--- /dev/null
+++ b/examples/react-eject-minimal/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+ React Minimal Ejected
+
+
+
+
+
+
+
+
+
diff --git a/examples/react-eject-minimal/package.json b/examples/react-eject-minimal/package.json
new file mode 100644
index 00000000..2fe5ef4c
--- /dev/null
+++ b/examples/react-eject-minimal/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "react-eject-minimal",
+ "type": "module",
+ "version": "1.0.0",
+ "private": true,
+ "description": "React example for Video.js 10",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@videojs/react": "workspace:*",
+ "clsx": "^2.1.1",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "screenfull": "^6.0.2",
+ "tailwindcss": "^4.1.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.0",
+ "@types/react": "^18.0.0",
+ "@types/react-dom": "^18.0.0",
+ "@vitejs/plugin-react": "^5.0.3",
+ "typescript": "^5.9.2",
+ "vite": "^7.1.6"
+ }
+}
diff --git a/examples/react-eject-minimal/src/MinimalSkin.tsx b/examples/react-eject-minimal/src/MinimalSkin.tsx
new file mode 100644
index 00000000..329e027c
--- /dev/null
+++ b/examples/react-eject-minimal/src/MinimalSkin.tsx
@@ -0,0 +1,120 @@
+import type { PropsWithChildren } from 'react';
+
+import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
+import {
+ FullscreenEnterAltIcon,
+ FullscreenExitAltIcon,
+ PauseIcon,
+ PlayIcon,
+ VolumeHighIcon,
+ VolumeLowIcon,
+ VolumeOffIcon,
+} from '@videojs/react/icons';
+
+import './minimal.css';
+
+type SkinProps = PropsWithChildren<{
+ className?: string;
+}>;
+
+export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element {
+ return (
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Play
+ Pause
+
+
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter Fullscreen
+ Exit Fullscreen
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/react-eject-minimal/src/main.tsx b/examples/react-eject-minimal/src/main.tsx
new file mode 100644
index 00000000..a827a7de
--- /dev/null
+++ b/examples/react-eject-minimal/src/main.tsx
@@ -0,0 +1,20 @@
+import { MediaProvider, Video } from '@videojs/react';
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import MinimalSkin from './MinimalSkin';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+ ,
+);
diff --git a/examples/react-eject-minimal/src/minimal.css b/examples/react-eject-minimal/src/minimal.css
new file mode 100644
index 00000000..a31687bd
--- /dev/null
+++ b/examples/react-eject-minimal/src/minimal.css
@@ -0,0 +1,349 @@
+.vjs-minimal-skin * {
+ box-sizing: border-box;
+}
+
+.vjs-minimal-skin {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+.vjs-minimal-skin::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
+}
+@media (prefers-color-scheme: dark) {
+ .vjs-minimal-skin::after {
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Fullscreen */
+.vjs-minimal-skin:fullscreen {
+ border-radius: 0;
+}
+
+.vjs-minimal-skin > ::slotted([slot='media']) {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+.vjs-minimal-skin > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
+ transform: translateY(100%);
+ transition-property: transform, opacity;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+.vjs-minimal-skin:hover > .overlay,
+.vjs-minimal-skin:has([data-paused]) > .overlay,
+.vjs-minimal-skin:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transform: translateY(0);
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Media Control Bar UI/Styles */
+.vjs-minimal-skin .control-bar {
+ position: absolute;
+ bottom: 0;
+ inset-inline: 0;
+ padding: 2.5rem 0.75rem 0.75rem 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.875rem;
+ will-change: transform, filter, opacity;
+ transform: translateY(100%);
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ color: oklab(1 0 0);
+}
+.vjs-minimal-skin:hover > .control-bar,
+.vjs-minimal-skin:has([data-paused]) > .control-bar,
+.vjs-minimal-skin:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.vjs-minimal-skin .time-display-group {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+.vjs-minimal-skin .duration-display {
+ display: contents;
+ color: oklab(1 0 0 / 0.5);
+}
+.vjs-minimal-skin .time-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
+ font-variant-numeric: tabular-nums;
+}
+
+.vjs-minimal-skin .button-group {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+/* Generic Media Button Styling */
+.vjs-minimal-skin .button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.625rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: 0.375rem;
+ color: oklab(1 0 0);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-minimal-skin .button:hover,
+.vjs-minimal-skin .button:focus-visible,
+.vjs-minimal-skin .button[aria-expanded='true'] {
+ color: oklab(1 0 0 / 0.8);
+ text-decoration: none;
+}
+.vjs-minimal-skin .button:focus-visible {
+ outline-color: oklab(1 0 0);
+ outline-offset: 2px;
+}
+.vjs-minimal-skin .button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.vjs-minimal-skin .button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
+}
+
+/* Media Play Button UI/Styles */
+.vjs-minimal-skin .play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+.vjs-minimal-skin .play-button:not([data-paused]) .pause-icon,
+.vjs-minimal-skin .play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+.vjs-minimal-skin .fullscreen-button .icon {
+ display: none;
+}
+.vjs-minimal-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+.vjs-minimal-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+.vjs-minimal-skin .mute-button .icon {
+ display: none;
+}
+.vjs-minimal-skin .mute-button:not([data-volume-level]) .volume-low-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='high'] .volume-high-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='low'] .volume-low-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='medium'] .volume-low-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.vjs-minimal-skin .slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.vjs-minimal-skin .slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.vjs-minimal-skin .slider[data-orientation='vertical'] {
+ height: 4.5rem;
+ width: 1.25rem;
+}
+
+.vjs-minimal-skin .slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.vjs-minimal-skin .slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.1875rem;
+}
+
+/* Vertical track styles */
+.vjs-minimal-skin .slider-track[data-orientation='vertical'] {
+ width: 0.1875rem;
+ height: 100%;
+}
+
+.vjs-minimal-skin .slider:focus-visible .slider-track {
+ outline-color: oklab(1 0 0);
+ outline-offset: 6px;
+}
+
+.vjs-minimal-skin .slider-thumb {
+ width: 0.75rem;
+ height: 0.75rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ scale: 0.7;
+ transform-origin: center;
+ transition-property: opacity, scale;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-minimal-skin .slider:hover .slider-thumb,
+.vjs-minimal-skin .slider:focus-within .slider-thumb {
+ opacity: 1;
+ scale: 1;
+}
+.vjs-minimal-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.vjs-minimal-skin .slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.vjs-minimal-skin .slider-pointer {
+ display: none;
+}
+
+.vjs-minimal-skin .slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+.vjs-minimal-skin .media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.vjs-minimal-skin .popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.vjs-minimal-skin .popup-animation[data-starting-style],
+.vjs-minimal-skin .popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.vjs-minimal-skin .popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+.vjs-minimal-skin .popover-popup {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+.vjs-minimal-skin .tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ 0 4px 6px -1px oklab(0 0 0 / 0.1),
+ 0 2px 4px -2px oklab(0 0 0 / 0.1);
+}
+@media (prefers-reduced-transparency: reduce) {
+ .vjs-minimal-skin .tooltip-popup {
+ background-color: oklab(0 0 0 / 0.7);
+ }
+}
+@media (prefers-contrast: more) {
+ .vjs-minimal-skin .tooltip-popup {
+ background-color: oklab(0 0 0 / 0.9);
+ }
+}
+
+.vjs-minimal-skin .tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+.vjs-minimal-skin .tooltip-popup[data-paused] .play-tooltip,
+.vjs-minimal-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+.vjs-minimal-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+.vjs-minimal-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}
diff --git a/examples/react-eject-minimal/tsconfig.json b/examples/react-eject-minimal/tsconfig.json
new file mode 100644
index 00000000..40aa8ebc
--- /dev/null
+++ b/examples/react-eject-minimal/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "baseUrl": "."
+ },
+ "include": ["src"]
+}
diff --git a/examples/react-eject-minimal/vite.config.ts b/examples/react-eject-minimal/vite.config.ts
new file mode 100644
index 00000000..6869bb0a
--- /dev/null
+++ b/examples/react-eject-minimal/vite.config.ts
@@ -0,0 +1,15 @@
+import tailwindcss from '@tailwindcss/vite';
+
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
+// https://vitejs.dev/config
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ server: {
+ port: 5173,
+ },
+ optimizeDeps: {
+ exclude: ['@videojs/react'],
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 24e82aea..f0088dbb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -94,6 +94,32 @@ importers:
specifier: ^7.1.6
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
+ examples/html-eject-frosted:
+ dependencies:
+ '@videojs/html':
+ specifier: workspace:*
+ version: link:../../packages/html
+ devDependencies:
+ typescript:
+ specifier: ^5.9.2
+ version: 5.9.3
+ vite:
+ specifier: ^7.1.6
+ version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
+
+ examples/html-eject-minimal:
+ dependencies:
+ '@videojs/html':
+ specifier: workspace:*
+ version: link:../../packages/html
+ devDependencies:
+ typescript:
+ specifier: ^5.9.2
+ version: 5.9.3
+ vite:
+ specifier: ^7.1.6
+ version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
+
examples/react-demo:
dependencies:
'@videojs/react':
@@ -134,6 +160,86 @@ importers:
specifier: ^7.1.6
version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
+ examples/react-eject-frosted:
+ dependencies:
+ '@videojs/react':
+ specifier: workspace:*
+ version: link:../../packages/react
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ react:
+ specifier: ^18.0.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.0.0
+ version: 18.3.1(react@18.3.1)
+ screenfull:
+ specifier: ^6.0.2
+ version: 6.0.2
+ tailwindcss:
+ specifier: ^4.1.13
+ version: 4.1.14
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.1.0
+ version: 4.1.14(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))
+ '@types/react':
+ specifier: ^18.0.0
+ version: 18.3.24
+ '@types/react-dom':
+ specifier: ^18.0.0
+ version: 18.3.7(@types/react@18.3.24)
+ '@vitejs/plugin-react':
+ specifier: ^5.0.3
+ version: 5.0.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))
+ typescript:
+ specifier: ^5.9.2
+ version: 5.9.3
+ vite:
+ specifier: ^7.1.6
+ version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
+
+ examples/react-eject-minimal:
+ dependencies:
+ '@videojs/react':
+ specifier: workspace:*
+ version: link:../../packages/react
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ react:
+ specifier: ^18.0.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.0.0
+ version: 18.3.1(react@18.3.1)
+ screenfull:
+ specifier: ^6.0.2
+ version: 6.0.2
+ tailwindcss:
+ specifier: ^4.1.13
+ version: 4.1.14
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.1.0
+ version: 4.1.14(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))
+ '@types/react':
+ specifier: ^18.0.0
+ version: 18.3.24
+ '@types/react-dom':
+ specifier: ^18.0.0
+ version: 18.3.7(@types/react@18.3.24)
+ '@vitejs/plugin-react':
+ specifier: ^5.0.3
+ version: 5.0.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))
+ typescript:
+ specifier: ^5.9.2
+ version: 5.9.3
+ vite:
+ specifier: ^7.1.6
+ version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)
+
packages/core:
dependencies:
'@videojs/utils':
diff --git a/site/src/components/HomePageDemo/Base.tsx b/site/src/components/HomePageDemo/Base.tsx
index 3448d52b..868faa4d 100644
--- a/site/src/components/HomePageDemo/Base.tsx
+++ b/site/src/components/HomePageDemo/Base.tsx
@@ -1,71 +1,59 @@
-import type { Media, Skin } from '@/stores/homePageDemos';
+import type { Skin } from '@/stores/homePageDemos';
import { useStore } from '@nanostores/react';
import { TabsPanel, TabsRoot } from '@/components/Tabs';
-import { framework, media, skin } from '@/stores/homePageDemos';
+import { framework, skin } from '@/stores/homePageDemos';
import ClientCode from '../Code/ClientCode';
-function generateHTMLCode(skin: Skin, media: Media): string {
+function generateHTMLCode(skin: Skin): string {
const skinTag = `${skin}-skin`;
- const mediaTag = media;
- const videoExtension = media === 'hls-video' ? 'm3u8' : 'mp4';
return `
<${skinTag}>
- <${mediaTag} src="https://example.com/video.${videoExtension}">${mediaTag}>
+
${skinTag}>
`;
}
-function generateReactCode(skin: Skin, media: Media): string {
+function generateReactCode(skin: Skin): string {
const skinComponent = skin === 'frosted' ? 'FrostedSkin' : 'MinimalSkin';
- const mediaComponent = media === 'video' ? 'Video' : 'HLSVideo';
const skinImport = skin === 'frosted' ? 'frosted' : 'minimal';
- const videoExtension = media === 'hls-video' ? 'm3u8' : 'mp4';
- return `import { VideoProvider, ${mediaComponent} } from '@videojs/react';
+ return `import { VideoProvider, Video } from '@videojs/react';
import { ${skinComponent} } from '@videojs/react/skins/${skinImport}';
export const VideoPlayer = () => {
return (
<${skinComponent}>
- <${mediaComponent} src="https://example.com/video.${videoExtension}" />
+
${skinComponent}>
);
};`;
}
-function generateCSS(_skin: Skin, _media: Media): string {
- return 'Coming soon';
-}
-
-function generateJS(_skin: Skin, _media: Media): string {
- return 'Coming soon';
+function generateJS(_skin: Skin): string {
+ return `import '@videojs/html/skins/frosted';`;
}
export default function BaseDemo({ className }: { className?: string }) {
const $framework = useStore(framework);
const $skin = useStore(skin);
- const $media = useStore(media);
if ($framework === 'html') {
return (
-
-
-
-
+
-
+
);
@@ -80,7 +68,7 @@ export default function BaseDemo({ className }: { className?: string }) {
maxWidth={false}
>
-
+
);
diff --git a/site/src/components/HomePageDemo/Eject.tsx b/site/src/components/HomePageDemo/Eject.tsx
index 54ec3fc1..ed2a81df 100644
--- a/site/src/components/HomePageDemo/Eject.tsx
+++ b/site/src/components/HomePageDemo/Eject.tsx
@@ -1,33 +1,39 @@
-import type { Media, Skin } from '@/stores/homePageDemos';
+import type { Skin } from '@/stores/homePageDemos';
import { useStore } from '@nanostores/react';
import { TabsPanel, TabsRoot } from '@/components/Tabs';
-import { framework, media, skin } from '@/stores/homePageDemos';
+import { framework, skin } from '@/stores/homePageDemos';
+import {
+ generateHTMLCSS,
+ generateHTMLJS,
+ generateHTMLMarkup,
+ generateReactComponent,
+ generateReactCSS as genReactCSS,
+} from '@/utils/ejectCodeGenerator';
import ClientCode from '../Code/ClientCode';
-function generateHTMLCode(_skin: Skin, _media: Media): string {
- return 'Coming soon';
+function generateReactCode(skin: Skin): string {
+ return generateReactComponent(skin);
}
-function generateReactCode(_skin: Skin, _media: Media): string {
- return 'Coming soon';
+function generateReactCSS(skin: Skin): string {
+ return genReactCSS(skin);
}
-function generateCSSModuleCode(_skin: Skin, _media: Media): string {
- return 'Coming soon';
+function generateHTMLCode(skin: Skin): string {
+ return generateHTMLMarkup(skin);
}
-function generateCSS(_skin: Skin, _media: Media): string {
- return 'Coming soon';
+function generateCSS(skin: Skin): string {
+ return generateHTMLCSS(skin);
}
-function generateJS(_skin: Skin, _media: Media): string {
- return 'Coming soon';
+function generateJS(skin: Skin): string {
+ return generateHTMLJS(skin);
}
export default function EjectDemo({ className }: { className?: string }) {
const $framework = useStore(framework);
const $skin = useStore(skin);
- const $media = useStore(media);
if ($framework === 'html') {
return (
@@ -39,13 +45,13 @@ export default function EjectDemo({ className }: { className?: string }) {
maxWidth={false}
>
-
+
-
+
-
+
);
@@ -55,15 +61,15 @@ export default function EjectDemo({ className }: { className?: string }) {
-
+
-
+
);
diff --git a/site/src/content/docs/how-to/customize-skins.mdx b/site/src/content/docs/how-to/customize-skins.mdx
index 6cb3b6ce..1e19dc90 100644
--- a/site/src/content/docs/how-to/customize-skins.mdx
+++ b/site/src/content/docs/how-to/customize-skins.mdx
@@ -3,4 +3,96 @@ title: Customize Skins
description: Learn how to customize Video.js v10 skins by copying and modifying them
---
-I'm going to put the eject examples here in another PR
\ No newline at end of file
+import FrameworkCase from '@/components/docs/FrameworkCase.astro';
+import ServerCode from '@/components/Code/ServerCode.astro';
+import { TabsRoot, TabsPanel } from '@/components/Tabs.tsx';
+import {
+ generateHTMLCSS,
+ generateHTMLJS,
+ generateHTMLMarkup,
+ generateReactComponent,
+ generateReactCSS,
+} from '@/utils/ejectCodeGenerator';
+
+
+Video.js v10 comes with pre-built skins like Frosted and Minimal, but you can fully customize them by "ejecting" the code and making it your own. While eventually we'll have a CLI that'll eject skins in your preferred framework and style, for now we invite you to try it out with these copy-paste-ready implementations.
+
+> ⚠️ **Notice:** The HTML examples below aren't quite working, yet. Stay tuned for updates.
+
+## Frosted Skin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Minimal Skin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/site/src/stores/homePageDemos.ts b/site/src/stores/homePageDemos.ts
index 2aca562b..c33432f2 100644
--- a/site/src/stores/homePageDemos.ts
+++ b/site/src/stores/homePageDemos.ts
@@ -2,8 +2,6 @@ import { atom } from 'nanostores';
export type Framework = 'html' | 'react';
export type Skin = 'frosted' | 'minimal';
-export type Media = 'video' | 'hls-video';
export const framework = atom('react');
export const skin = atom('frosted');
-export const media = atom('video');
diff --git a/site/src/utils/ejectCodeGenerator.ts b/site/src/utils/ejectCodeGenerator.ts
new file mode 100644
index 00000000..cc5ab429
--- /dev/null
+++ b/site/src/utils/ejectCodeGenerator.ts
@@ -0,0 +1,1919 @@
+/**
+ * Shared code generation module for ejected skin examples.
+ * This is the single source of truth for code displayed on the home page demo and in docs.
+ */
+
+type Skin = 'frosted' | 'minimal';
+
+/**
+ * Generate React component code for the specified skin.
+ * Always uses .m3u8 video source.
+ */
+export function generateReactComponent(skin: Skin): string {
+ if (skin === 'frosted') {
+ return `import type { PropsWithChildren } from 'react';
+
+import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
+import {
+ FullscreenEnterIcon,
+ FullscreenExitIcon,
+ PauseIcon,
+ PlayIcon,
+ VolumeHighIcon,
+ VolumeLowIcon,
+ VolumeOffIcon,
+} from '@videojs/react/icons';
+
+import './frosted.css';
+
+type SkinProps = PropsWithChildren<{
+ className?: string;
+}>;
+
+export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element {
+ return (
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Play
+ Pause
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter Fullscreen
+ Exit Fullscreen
+
+
+
+
+
+
+ );
+}`;
+ } else {
+ return `import type { PropsWithChildren } from 'react';
+
+import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react';
+import {
+ FullscreenEnterAltIcon,
+ FullscreenExitAltIcon,
+ PauseIcon,
+ PlayIcon,
+ VolumeHighIcon,
+ VolumeLowIcon,
+ VolumeOffIcon,
+} from '@videojs/react/icons';
+
+import './minimal.css';
+
+type SkinProps = PropsWithChildren<{
+ className?: string;
+}>;
+
+export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element {
+ return (
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Play
+ Pause
+
+
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter Fullscreen
+ Exit Fullscreen
+
+
+
+
+
+
+
+ );
+}`;
+ }
+}
+
+/**
+ * Generate React CSS code for the specified skin.
+ */
+export function generateReactCSS(skin: Skin): string {
+ if (skin === 'frosted') {
+ return `.vjs-frosted-skin * {
+ box-sizing: border-box;
+}
+
+.vjs-frosted-skin {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+.vjs-frosted-skin::before,
+.vjs-frosted-skin::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+}
+.vjs-frosted-skin::before {
+ inset: 1px;
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
+}
+.vjs-frosted-skin::after {
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
+}
+
+/* Fullscreen */
+.vjs-frosted-skin:fullscreen {
+ border-radius: 0;
+}
+
+.vjs-frosted-skin > ::slotted([slot='media']) {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+.vjs-frosted-skin > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
+ backdrop-filter: saturate(1.5) brightness(0.9);
+ transition: opacity 0.15s ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+.vjs-frosted-skin:hover > .overlay,
+.vjs-frosted-skin:has([data-paused]) > .overlay,
+.vjs-frosted-skin:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Common Surface Styles - e.g. tooltips, popovers, controls */
+.vjs-frosted-skin .surface {
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ inset 0 0 0 1px oklab(1 0 0 / 0.15),
+ 0 0 0 1px oklab(0 0 0 / 0.15),
+ oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
+ oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
+}
+@media (prefers-reduced-transparency: reduce) {
+ .vjs-frosted-skin .surface {
+ background-color: oklab(0 0 0 / 0.7);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+@media (prefers-contrast: more) {
+ .vjs-frosted-skin .surface {
+ background-color: oklab(0 0 0 / 0.9);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Media Control Bar UI/Styles */
+.vjs-frosted-skin .control-bar {
+ position: absolute;
+ bottom: 0.75rem;
+ inset-inline: 0.75rem;
+ padding: 0.25rem;
+ display: flex;
+ align-items: center;
+ gap: 0.125rem;
+ border-radius: calc(infinity * 1px);
+ will-change: scale, transform, filter, opacity;
+ scale: 0.9;
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: scale, transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ transform-origin: bottom;
+ color: oklab(1 0 0);
+}
+.vjs-frosted-skin:hover > .control-bar,
+.vjs-frosted-skin:has([data-paused]) > .control-bar,
+.vjs-frosted-skin:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ scale: 1;
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.vjs-frosted-skin .time-controls {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 0.75rem;
+ padding-inline: 0.375rem;
+}
+.vjs-frosted-skin .time-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
+ font-variant-numeric: tabular-nums;
+}
+
+/* Generic Media Button Styling */
+.vjs-frosted-skin .button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.5rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: calc(infinity * 1px);
+ color: oklab(1 0 0 / 0.9);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-frosted-skin .button:hover,
+.vjs-frosted-skin .button:focus-visible,
+.vjs-frosted-skin .button[aria-expanded='true'] {
+ background-color: oklab(1 0 0 / 0.1);
+ color: oklab(1 0 0);
+ text-decoration: none;
+}
+.vjs-frosted-skin .button:focus-visible {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 2px;
+}
+.vjs-frosted-skin .button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.vjs-frosted-skin .button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
+}
+
+/* Media Play Button UI/Styles */
+.vjs-frosted-skin .play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+.vjs-frosted-skin .play-button:not([data-paused]) .pause-icon,
+.vjs-frosted-skin .play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+.vjs-frosted-skin .fullscreen-button .icon {
+ display: none;
+}
+.vjs-frosted-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+.vjs-frosted-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+.vjs-frosted-skin .mute-button .icon {
+ display: none;
+}
+.vjs-frosted-skin .mute-button:not([data-volume-level]) .volume-low-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='high'] .volume-high-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='low'] .volume-low-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='medium'] .volume-low-icon,
+.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.vjs-frosted-skin .slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.vjs-frosted-skin .slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.vjs-frosted-skin .slider[data-orientation='vertical'] {
+ height: 5rem;
+ width: 1.25rem;
+}
+
+.vjs-frosted-skin .slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.vjs-frosted-skin .slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.25rem;
+}
+
+/* Vertical track styles */
+.vjs-frosted-skin .slider-track[data-orientation='vertical'] {
+ width: 0.25rem;
+ height: 100%;
+}
+
+.vjs-frosted-skin .slider:focus-visible .slider-track {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 6px;
+}
+
+.vjs-frosted-skin .slider-thumb {
+ width: 0.625rem;
+ height: 0.625rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ transition-property: opacity, height, width;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-frosted-skin .slider-thumb:active {
+ width: 0.75rem;
+ height: 0.75rem;
+}
+.vjs-frosted-skin .slider:hover .slider-thumb,
+.vjs-frosted-skin .slider:focus-within .slider-thumb {
+ opacity: 1;
+}
+.vjs-frosted-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.vjs-frosted-skin .slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.vjs-frosted-skin .slider-pointer {
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+}
+
+.vjs-frosted-skin .slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+.vjs-frosted-skin .media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.vjs-frosted-skin .popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.vjs-frosted-skin .popup-animation[data-starting-style],
+.vjs-frosted-skin .popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.vjs-frosted-skin .popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+.vjs-frosted-skin .popover-popup {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+.vjs-frosted-skin .tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.625rem;
+ border-radius: calc(infinity * 1px);
+ font-size: 0.75rem;
+}
+
+.vjs-frosted-skin .tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+.vjs-frosted-skin .tooltip-popup[data-paused] .play-tooltip,
+.vjs-frosted-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+.vjs-frosted-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+.vjs-frosted-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}`;
+ } else {
+ return `.vjs-minimal-skin * {
+ box-sizing: border-box;
+}
+
+.vjs-minimal-skin {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+.vjs-minimal-skin::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
+}
+@media (prefers-color-scheme: dark) {
+ .vjs-minimal-skin::after {
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Fullscreen */
+.vjs-minimal-skin:fullscreen {
+ border-radius: 0;
+}
+
+.vjs-minimal-skin > ::slotted([slot='media']) {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+.vjs-minimal-skin > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
+ transform: translateY(100%);
+ transition-property: transform, opacity;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+.vjs-minimal-skin:hover > .overlay,
+.vjs-minimal-skin:has([data-paused]) > .overlay,
+.vjs-minimal-skin:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transform: translateY(0);
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Media Control Bar UI/Styles */
+.vjs-minimal-skin .control-bar {
+ position: absolute;
+ bottom: 0;
+ inset-inline: 0;
+ padding: 2.5rem 0.75rem 0.75rem 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.875rem;
+ will-change: transform, filter, opacity;
+ transform: translateY(100%);
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ color: oklab(1 0 0);
+}
+.vjs-minimal-skin:hover > .control-bar,
+.vjs-minimal-skin:has([data-paused]) > .control-bar,
+.vjs-minimal-skin:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.vjs-minimal-skin .time-display-group {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+.vjs-minimal-skin .duration-display {
+ display: contents;
+ color: oklab(1 0 0 / 0.5);
+}
+.vjs-minimal-skin .time-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
+ font-variant-numeric: tabular-nums;
+}
+
+.vjs-minimal-skin .button-group {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+/* Generic Media Button Styling */
+.vjs-minimal-skin .button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.625rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: 0.375rem;
+ color: oklab(1 0 0);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-minimal-skin .button:hover,
+.vjs-minimal-skin .button:focus-visible,
+.vjs-minimal-skin .button[aria-expanded='true'] {
+ color: oklab(1 0 0 / 0.8);
+ text-decoration: none;
+}
+.vjs-minimal-skin .button:focus-visible {
+ outline-color: oklab(1 0 0);
+ outline-offset: 2px;
+}
+.vjs-minimal-skin .button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.vjs-minimal-skin .button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
+}
+
+/* Media Play Button UI/Styles */
+.vjs-minimal-skin .play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+.vjs-minimal-skin .play-button:not([data-paused]) .pause-icon,
+.vjs-minimal-skin .play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+.vjs-minimal-skin .fullscreen-button .icon {
+ display: none;
+}
+.vjs-minimal-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+.vjs-minimal-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+.vjs-minimal-skin .mute-button .icon {
+ display: none;
+}
+.vjs-minimal-skin .mute-button:not([data-volume-level]) .volume-low-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='high'] .volume-high-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='low'] .volume-low-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='medium'] .volume-low-icon,
+.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.vjs-minimal-skin .slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.vjs-minimal-skin .slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.vjs-minimal-skin .slider[data-orientation='vertical'] {
+ height: 4.5rem;
+ width: 1.25rem;
+}
+
+.vjs-minimal-skin .slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.vjs-minimal-skin .slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.1875rem;
+}
+
+/* Vertical track styles */
+.vjs-minimal-skin .slider-track[data-orientation='vertical'] {
+ width: 0.1875rem;
+ height: 100%;
+}
+
+.vjs-minimal-skin .slider:focus-visible .slider-track {
+ outline-color: oklab(1 0 0);
+ outline-offset: 6px;
+}
+
+.vjs-minimal-skin .slider-thumb {
+ width: 0.75rem;
+ height: 0.75rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ scale: 0.7;
+ transform-origin: center;
+ transition-property: opacity, scale;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.vjs-minimal-skin .slider:hover .slider-thumb,
+.vjs-minimal-skin .slider:focus-within .slider-thumb {
+ opacity: 1;
+ scale: 1;
+}
+.vjs-minimal-skin .slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.vjs-minimal-skin .slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.vjs-minimal-skin .slider-pointer {
+ display: none;
+}
+
+.vjs-minimal-skin .slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+.vjs-minimal-skin .media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.vjs-minimal-skin .popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.vjs-minimal-skin .popup-animation[data-starting-style],
+.vjs-minimal-skin .popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.vjs-minimal-skin .popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+.vjs-minimal-skin .popover-popup {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+.vjs-minimal-skin .tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ 0 4px 6px -1px oklab(0 0 0 / 0.1),
+ 0 2px 4px -2px oklab(0 0 0 / 0.1);
+}
+@media (prefers-reduced-transparency: reduce) {
+ .vjs-minimal-skin .tooltip-popup {
+ background-color: oklab(0 0 0 / 0.7);
+ }
+}
+@media (prefers-contrast: more) {
+ .vjs-minimal-skin .tooltip-popup {
+ background-color: oklab(0 0 0 / 0.9);
+ }
+}
+
+.vjs-minimal-skin .tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+.vjs-minimal-skin .tooltip-popup[data-paused] .play-tooltip,
+.vjs-minimal-skin .tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+.vjs-minimal-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+.vjs-minimal-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}`;
+ }
+}
+
+/**
+ * Generate HTML markup for the specified skin.
+ * Always uses .mp4 video source.
+ */
+export function generateHTMLMarkup(skin: Skin): string {
+ if (skin === 'frosted') {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ } else {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ }
+}
+
+/**
+ * Generate HTML CSS code for the specified skin.
+ */
+export function generateHTMLCSS(skin: Skin): string {
+ // HTML CSS is nearly identical to React CSS, with just element selectors instead of class selectors
+ if (skin === 'frosted') {
+ return `media-container * {
+ box-sizing: border-box;
+}
+
+media-container {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+media-container::before,
+media-container::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+}
+media-container::before {
+ inset: 1px;
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15);
+}
+media-container::after {
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1);
+}
+
+/* Fullscreen */
+media-container:fullscreen {
+ border-radius: 0;
+}
+
+media-container > ::slotted([slot='media']),
+media-container > video,
+media-container > audio {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+media-container > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0));
+ backdrop-filter: saturate(1.5) brightness(0.9);
+ transition: opacity 0.15s ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+media-container:hover > .overlay,
+media-container:has([data-paused]) > .overlay,
+media-container:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Common Surface Styles - e.g. tooltips, popovers, controls */
+.surface {
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ inset 0 0 0 1px oklab(1 0 0 / 0.15),
+ 0 0 0 1px oklab(0 0 0 / 0.15),
+ oklab(0 0 0 / 0.15) 0px 1px 3px 0px,
+ oklab(0 0 0 / 0.15) 0px 1px 2px -1px;
+}
+@media (prefers-reduced-transparency: reduce) {
+ .surface {
+ background-color: oklab(0 0 0 / 0.7);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+@media (prefers-contrast: more) {
+ .surface {
+ background-color: oklab(0 0 0 / 0.9);
+ box-shadow:
+ inset 0 0 0 1px oklab(0 0 0),
+ 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Media Control Bar UI/Styles */
+.control-bar {
+ position: absolute;
+ bottom: 0.75rem;
+ inset-inline: 0.75rem;
+ padding: 0.25rem;
+ display: flex;
+ align-items: center;
+ gap: 0.125rem;
+ border-radius: calc(infinity * 1px);
+ will-change: scale, transform, filter, opacity;
+ scale: 0.9;
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: scale, transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ transform-origin: bottom;
+ color: oklab(1 0 0);
+}
+media-container:hover > .control-bar,
+media-container:has([data-paused]) > .control-bar,
+media-container:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ scale: 1;
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.time-controls {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 0.75rem;
+ padding-inline: 0.375rem;
+}
+media-current-time-display,
+media-duration-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
+ font-variant-numeric: tabular-nums;
+}
+
+/* Generic Media Button Styling */
+.button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.5rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: calc(infinity * 1px);
+ color: oklab(1 0 0 / 0.9);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.button:hover,
+.button:focus-visible,
+.button[aria-expanded='true'] {
+ background-color: oklab(1 0 0 / 0.1);
+ color: oklab(1 0 0);
+ text-decoration: none;
+}
+.button:focus-visible {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 2px;
+}
+.button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25));
+}
+
+/* Media Play Button UI/Styles */
+media-play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+media-play-button:not([data-paused]) .pause-icon,
+media-play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+media-fullscreen-button .icon {
+ display: none;
+}
+media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+media-mute-button .icon {
+ display: none;
+}
+media-mute-button:not([data-volume-level]) .volume-low-icon,
+media-mute-button[data-volume-level='high'] .volume-high-icon,
+media-mute-button[data-volume-level='low'] .volume-low-icon,
+media-mute-button[data-volume-level='medium'] .volume-low-icon,
+media-mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.slider[data-orientation='vertical'] {
+ height: 5rem;
+ width: 1.25rem;
+}
+
+.slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.25rem;
+}
+
+/* Vertical track styles */
+.slider-track[data-orientation='vertical'] {
+ width: 0.25rem;
+ height: 100%;
+}
+
+.slider:focus-visible .slider-track {
+ outline-color: oklch(62.3% 0.214 259.815);
+ outline-offset: 6px;
+}
+
+.slider-thumb {
+ width: 0.625rem;
+ height: 0.625rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ transition-property: opacity, height, width;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.slider-thumb:active {
+ width: 0.75rem;
+ height: 0.75rem;
+}
+.slider:hover .slider-thumb,
+.slider:focus-within .slider-thumb {
+ opacity: 1;
+}
+.slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.slider-pointer {
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+}
+
+.slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.popup-animation[data-starting-style],
+.popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+media-popover {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+media-tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.625rem;
+ border-radius: calc(infinity * 1px);
+ font-size: 0.75rem;
+}
+
+.tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+media-tooltip-popup[data-paused] .play-tooltip,
+media-tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}`;
+ } else {
+ return `media-container * {
+ box-sizing: border-box;
+}
+
+media-container {
+ position: relative;
+ isolation: isolate;
+ container: root / inline-size;
+ overflow: clip;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border-radius: inherit;
+ background: oklab(0 0 0);
+}
+media-container::after {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ border-radius: inherit;
+ z-index: 10;
+ inset: 0;
+ box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2);
+}
+@media (prefers-color-scheme: dark) {
+ media-container::after {
+ box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2);
+ }
+}
+
+/* Fullscreen */
+media-container:fullscreen {
+ border-radius: 0;
+}
+
+media-container > ::slotted([slot='media']),
+media-container > video,
+media-container > audio {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* Media Container UI Overlay Styling */
+media-container > .overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: start;
+ pointer-events: none;
+ border-radius: inherit;
+ background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0));
+ transform: translateY(100%);
+ transition-property: transform, opacity;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+ transition-delay: 500ms;
+ opacity: 0;
+}
+media-container:hover > .overlay,
+media-container:has([data-paused]) > .overlay,
+media-container:has([aria-expanded='true']) > .overlay {
+ opacity: 1;
+ transform: translateY(0);
+ transition-duration: 100ms;
+ transition-delay: 0ms;
+}
+
+/* Media Control Bar UI/Styles */
+.control-bar {
+ position: absolute;
+ bottom: 0;
+ inset-inline: 0;
+ padding: 2.5rem 0.75rem 0.75rem 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.875rem;
+ will-change: transform, filter, opacity;
+ transform: translateY(100%);
+ opacity: 0;
+ filter: blur(8px);
+ transition-property: transform, filter, opacity;
+ transition-delay: 500ms;
+ transition-duration: 300ms;
+ transition-timing-function: ease-out;
+ color: oklab(1 0 0);
+}
+media-container:hover > .control-bar,
+media-container:has([data-paused]) > .control-bar,
+media-container:has([aria-expanded='true']) > .control-bar {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0px);
+ transition-delay: 0ms;
+ transition-duration: 100ms;
+}
+
+/* Time Display Styling */
+.time-display-group {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+.duration-display {
+ display: contents;
+ color: oklab(1 0 0 / 0.5);
+}
+media-current-time-display,
+media-duration-display {
+ text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
+ font-variant-numeric: tabular-nums;
+}
+
+.button-group {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+/* Generic Media Button Styling */
+.button {
+ display: grid;
+ flex-shrink: 0;
+ padding: 0.625rem;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ border-radius: 0.375rem;
+ color: oklab(1 0 0);
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition-property: background-color, color, outline-offset;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.button:hover,
+.button:focus-visible,
+.button[aria-expanded='true'] {
+ color: oklab(1 0 0 / 0.8);
+ text-decoration: none;
+}
+.button:focus-visible {
+ outline-color: oklab(1 0 0);
+ outline-offset: 2px;
+}
+.button[disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+ filter: grayscale(1);
+}
+
+.button .icon {
+ grid-area: 1 / 1;
+ width: 18px;
+ height: 18px;
+ filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4));
+}
+
+/* Media Play Button UI/Styles */
+media-play-button .icon {
+ opacity: 0;
+ transition: opacity 150ms linear;
+}
+
+media-play-button:not([data-paused]) .pause-icon,
+media-play-button[data-paused] .play-icon {
+ opacity: 1;
+}
+
+/* Media Fullscreen Button UI/Styles */
+media-fullscreen-button .icon {
+ display: none;
+}
+media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
+media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
+ display: inline;
+}
+
+/* One way to define the "default visible" icon (CJP) */
+media-mute-button .icon {
+ display: none;
+}
+media-mute-button:not([data-volume-level]) .volume-low-icon,
+media-mute-button[data-volume-level='high'] .volume-high-icon,
+media-mute-button[data-volume-level='low'] .volume-low-icon,
+media-mute-button[data-volume-level='medium'] .volume-low-icon,
+media-mute-button[data-volume-level='off'] .volume-off-icon {
+ display: inline;
+}
+
+/* TimeSlider Component Styles */
+.slider {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ border-radius: calc(infinity * 1px);
+ outline: none;
+}
+
+/* Horizontal orientation styles */
+.slider[data-orientation='horizontal'] {
+ min-width: 5rem;
+ width: 100%;
+ height: 1.25rem;
+}
+
+/* Vertical orientation styles */
+.slider[data-orientation='vertical'] {
+ height: 4.5rem;
+ width: 1.25rem;
+}
+
+.slider-track {
+ position: relative;
+ isolation: isolate;
+ background-color: oklab(1 0 0 / 0.2);
+ border-radius: inherit;
+ overflow: hidden;
+ user-select: none;
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ transition: outline-offset 150ms ease-out;
+ box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05);
+}
+
+/* Horizontal track styles */
+.slider-track[data-orientation='horizontal'] {
+ width: 100%;
+ height: 0.1875rem;
+}
+
+/* Vertical track styles */
+.slider-track[data-orientation='vertical'] {
+ width: 0.1875rem;
+ height: 100%;
+}
+
+.slider:focus-visible .slider-track {
+ outline-color: oklab(1 0 0);
+ outline-offset: 6px;
+}
+
+.slider-thumb {
+ width: 0.75rem;
+ height: 0.75rem;
+ background-color: oklab(1 0 0);
+ border-radius: calc(infinity * 1px);
+ user-select: none;
+ z-index: 10;
+ box-shadow:
+ 0 0 0 1px oklab(0 0 0 / 0.1),
+ 0 1px 3px 0 oklab(0 0 0 / 0.15),
+ 0 1px 2px -1px oklab(0 0 0 / 0.15);
+ opacity: 0;
+ scale: 0.7;
+ transform-origin: center;
+ transition-property: opacity, scale;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+.slider:hover .slider-thumb,
+.slider:focus-within .slider-thumb {
+ opacity: 1;
+ scale: 1;
+}
+.slider-track[data-orientation='horizontal'] .slider-thumb {
+ cursor: ew-resize;
+}
+.slider-track[data-orientation='vertical'] .slider-thumb {
+ cursor: ns-resize;
+}
+
+.slider-pointer {
+ display: none;
+}
+
+.slider-progress {
+ background-color: oklab(1 0 0);
+ border-radius: inherit;
+}
+
+media-preview-time-display {
+ font-variant-numeric: tabular-nums;
+}
+
+.popup-animation {
+ transition-property: transform, scale, opacity, filter;
+ transition-duration: 200ms;
+ transform: scale(1);
+ transform-origin: bottom;
+ opacity: 1;
+ filter: blur(0px);
+}
+.popup-animation[data-starting-style],
+.popup-animation[data-ending-style] {
+ transform: scale(0);
+ opacity: 0;
+ filter: blur(8px);
+}
+.popup-animation[data-instant] {
+ transition-duration: 0ms;
+}
+
+media-popover {
+ margin: 0;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ padding: 0.75rem 0.25rem;
+ border-radius: calc(infinity * 1px);
+}
+
+/* Tooltip Component Styles */
+media-tooltip-popup {
+ color: oklab(1 0 0);
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ background-color: oklab(1 0 0 / 0.1);
+ backdrop-filter: blur(64px) brightness(0.9) saturate(1.5);
+ box-shadow:
+ 0 4px 6px -1px oklab(0 0 0 / 0.1),
+ 0 2px 4px -2px oklab(0 0 0 / 0.1);
+}
+@media (prefers-reduced-transparency: reduce) {
+ media-tooltip-popup {
+ background-color: oklab(0 0 0 / 0.7);
+ }
+}
+@media (prefers-contrast: more) {
+ media-tooltip-popup {
+ background-color: oklab(0 0 0 / 0.9);
+ }
+}
+
+.tooltip {
+ display: none;
+ white-space: nowrap;
+}
+
+media-tooltip-popup[data-paused] .play-tooltip,
+media-tooltip-popup:not([data-paused]) .pause-tooltip {
+ display: block;
+}
+
+media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip,
+media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip {
+ display: block;
+}`;
+ }
+}
+
+/**
+ * Generate HTML JavaScript imports for the specified skin.
+ */
+export function generateHTMLJS(skin: Skin): string {
+ return `import './${skin}.css';
+import '@videojs/html/icons';
+import '@videojs/html/define/media-container';
+import '@videojs/html/define/media-provider';
+import '@videojs/html/define/media-play-button';
+import '@videojs/html/define/media-mute-button';
+import '@videojs/html/define/media-volume-slider';
+import '@videojs/html/define/media-time-slider';
+import '@videojs/html/define/media-fullscreen-button';
+import '@videojs/html/define/media-duration-display';
+import '@videojs/html/define/media-current-time-display';
+import '@videojs/html/define/media-preview-time-display';
+import '@videojs/html/define/media-popover';
+import '@videojs/html/define/media-tooltip';`;
+}