diff --git a/apps/e2e/tests/visual/video-skin.spec.ts b/apps/e2e/tests/visual/video-skin.spec.ts
index 5ac53861..4502e548 100644
--- a/apps/e2e/tests/visual/video-skin.spec.ts
+++ b/apps/e2e/tests/visual/video-skin.spec.ts
@@ -50,6 +50,87 @@ for (const { name, path } of VISUAL_PAGES) {
});
}
+// --- Portrait media layout ---
+
+test.describe('Visual — HTML Portrait Layout', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/pages/html-video-mp4.html', { waitUntil: 'domcontentloaded' });
+ await page.waitForFunction(() => customElements.get('video-skin'));
+ await page.evaluate(() => {
+ const root = document.getElementById('root');
+ if (!root) return;
+
+ root.innerHTML = `
+
+
+
+
+
+ `;
+ });
+ });
+
+ test('keeps the authored skin aspect ratio', async ({ page }) => {
+ const box = await page.evaluate(() => {
+ const container = document.querySelector('video-skin')?.shadowRoot?.querySelector('media-container');
+ const rect = container?.getBoundingClientRect();
+
+ return rect ? { height: rect.height, width: rect.width } : null;
+ });
+
+ expect(box).not.toBeNull();
+ expect(box!.width / box!.height).toBeCloseTo(16 / 9, 1);
+ });
+
+ test('caps portrait thumbnails to the configured max height', async ({ page }) => {
+ const src = `data:image/svg+xml,${encodeURIComponent(
+ ''
+ )}`;
+
+ await page.evaluate((url) => {
+ const thumbnail = document
+ .querySelector('video-skin')
+ ?.shadowRoot?.querySelector('media-slider-thumbnail') as HTMLElement & {
+ thumbnails?: Array<{ url: string; startTime: number; width: number; height: number }>;
+ };
+
+ if (!thumbnail) return;
+ thumbnail.thumbnails = [{ url, startTime: 0, width: 270, height: 480 }];
+ }, src);
+
+ await page.waitForFunction(() => {
+ const thumbnail = document.querySelector('video-skin')?.shadowRoot?.querySelector('media-slider-thumbnail');
+
+ return (
+ thumbnail &&
+ !thumbnail.hasAttribute('data-hidden') &&
+ !thumbnail.hasAttribute('data-loading') &&
+ parseFloat(getComputedStyle(thumbnail).height) > 0
+ );
+ });
+
+ const size = await page.evaluate(() => {
+ const thumbnail = document.querySelector('video-skin')!.shadowRoot!.querySelector('media-slider-thumbnail')!;
+ const style = getComputedStyle(thumbnail);
+ const probe = document.createElement('div');
+ probe.style.height = style.getPropertyValue('--media-slider-thumbnail-max-height');
+ document.body.append(probe);
+
+ const configuredMaxHeight = parseFloat(getComputedStyle(probe).height);
+ probe.remove();
+
+ return {
+ height: parseFloat(style.height),
+ configuredMaxHeight,
+ maxHeight: parseFloat(style.maxHeight),
+ };
+ });
+
+ expect(size.maxHeight).toBeCloseTo(size.configuredMaxHeight, 0);
+ expect(size.height).toBeLessThanOrEqual(size.maxHeight);
+ });
+});
+
// --- Captions snapshot (dedicated page with subtitle track baked in) ---
test.describe('Visual — Captions', () => {
diff --git a/apps/sandbox/app/shared/sources.ts b/apps/sandbox/app/shared/sources.ts
index c3511b90..eedd5ab9 100644
--- a/apps/sandbox/app/shared/sources.ts
+++ b/apps/sandbox/app/shared/sources.ts
@@ -31,6 +31,12 @@ export const SOURCES = {
type: 'hls',
subType: 'mp4',
},
+ 'hls-6': {
+ label: 'HLS - Tailwind (portrait)',
+ url: 'https://stream.mux.com/vth873zxidmhBVVRWBKcPTxnSQ302QqUm.m3u8',
+ type: 'hls',
+ subType: 'mp4',
+ },
'hls-multi-audio': {
label: 'HLS - Multi-language audio',
url: 'https://stream.mux.com/s41JYeqIpBMBzE4OzxDyGR2yrp2hD1CQ6gJN9SlVGDQ.m3u8',
diff --git a/packages/html/src/define/shared.css b/packages/html/src/define/shared.css
index 50795d15..ddbfa53b 100644
--- a/packages/html/src/define/shared.css
+++ b/packages/html/src/define/shared.css
@@ -12,6 +12,11 @@ media-tooltip-group {
width: 100%;
}
+media-container {
+ min-width: 0;
+ min-height: 0;
+}
+
/* Hide volume popover when volume control is unsupported (e.g., iOS Safari). */
.media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
diff --git a/packages/skins/src/default/css/video.css b/packages/skins/src/default/css/video.css
index 0c80c76a..abbbbab6 100644
--- a/packages/skins/src/default/css/video.css
+++ b/packages/skins/src/default/css/video.css
@@ -248,6 +248,7 @@
.media-default-skin--video .media-slider__thumbnail {
--media-slider-thumbnail-max-width: 11rem;
+ --media-slider-thumbnail-max-height: 8rem;
--media-slider-thumbnail-padding: -1.125rem;
/**
Inset is the difference between the container width and the slider (100%) width.
@@ -285,10 +286,7 @@
& .media-thumbnail__image {
max-width: var(--media-slider-thumbnail-max-width);
- }
-
- &:has(.media-thumbnail__image[data-loading]) {
- max-height: 6rem;
+ max-height: var(--media-slider-thumbnail-max-height);
}
}
.media-default-skin--video .media-slider[data-pointing] .media-slider__thumbnail:has([role="img"]:not([data-hidden])) {
diff --git a/packages/skins/src/default/tailwind/video.tailwind.ts b/packages/skins/src/default/tailwind/video.tailwind.ts
index dce6ff16..3b6493f3 100644
--- a/packages/skins/src/default/tailwind/video.tailwind.ts
+++ b/packages/skins/src/default/tailwind/video.tailwind.ts
@@ -155,16 +155,22 @@ export const thumbnail = {
root: cn(
baseThumbnail.root,
surface,
- '[--media-slider-thumbnail-max-width:11rem] [--media-slider-thumbnail-padding:-1.125rem] [--media-slider-thumbnail-inset:calc((100cqi-100%)/2)]',
+ '[--media-slider-thumbnail-max-width:11rem]',
+ '[--media-slider-thumbnail-max-height:8rem]',
+ '[--media-slider-thumbnail-padding:-1.125rem]',
+ '[--media-slider-thumbnail-inset:calc((100cqi-100%)/2)]',
'absolute [left:clamp(calc(var(--media-slider-thumbnail-max-width)/2+var(--media-slider-thumbnail-padding)-var(--media-slider-thumbnail-inset)),var(--media-slider-pointer),calc(100%-var(--media-slider-thumbnail-max-width)/2-var(--media-slider-thumbnail-padding)+var(--media-slider-thumbnail-inset)))] bottom-[calc(100%+1.2rem)] -translate-x-1/2',
'opacity-0 scale-80 blur-sm origin-bottom',
'transition-[scale,opacity,filter] duration-150',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:opacity-100',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:scale-100',
- 'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:blur-none',
- 'has-[[role=img][data-loading]]:max-h-24'
+ 'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:blur-none'
+ ),
+ image: cn(
+ baseThumbnail.image,
+ 'max-w-(--media-slider-thumbnail-max-width)',
+ 'max-h-(--media-slider-thumbnail-max-height)'
),
- image: cn(baseThumbnail.image, 'max-w-(--media-slider-thumbnail-max-width)'),
};
/* ==========================================================================
diff --git a/packages/skins/src/minimal/css/video.css b/packages/skins/src/minimal/css/video.css
index 65ea4e29..89a845e8 100644
--- a/packages/skins/src/minimal/css/video.css
+++ b/packages/skins/src/minimal/css/video.css
@@ -267,6 +267,7 @@
.media-minimal-skin--video .media-slider__thumbnail {
--media-slider-thumbnail-max-width: 11rem;
+ --media-slider-thumbnail-max-height: 8rem;
--media-slider-thumbnail-padding: -0.5rem;
/**
Inset is the difference between the container width and the slider (100%) width.
@@ -317,10 +318,7 @@
& .media-thumbnail__image {
max-width: var(--media-slider-thumbnail-max-width);
- }
-
- &:has(.media-thumbnail__image[data-loading]) {
- max-height: 6rem;
+ max-height: var(--media-slider-thumbnail-max-height);
}
}
.media-minimal-skin--video .media-slider[data-pointing] .media-slider__thumbnail:has([role="img"]:not([data-hidden])) {
diff --git a/packages/skins/src/minimal/tailwind/video.tailwind.ts b/packages/skins/src/minimal/tailwind/video.tailwind.ts
index 01d9613c..c7e8e32a 100644
--- a/packages/skins/src/minimal/tailwind/video.tailwind.ts
+++ b/packages/skins/src/minimal/tailwind/video.tailwind.ts
@@ -163,22 +163,28 @@ export const thumbnail = {
...baseThumbnail,
root: cn(
baseThumbnail.root,
- '[--media-slider-thumbnail-max-width:11rem] [--media-slider-thumbnail-padding:-0.5rem] [--media-slider-thumbnail-inset:calc(100cqi-100%)]',
+ '[--media-slider-thumbnail-max-width:11rem]',
+ '[--media-slider-thumbnail-max-height:8rem]',
+ '[--media-slider-thumbnail-padding:-0.5rem]',
+ '[--media-slider-thumbnail-inset:calc(100cqi-100%)]',
'absolute [left:clamp(calc(var(--media-slider-thumbnail-max-width)/2+var(--media-slider-thumbnail-padding)),var(--media-slider-pointer),calc(100%-var(--media-slider-thumbnail-max-width)/2-var(--media-slider-thumbnail-padding)+var(--media-slider-thumbnail-inset)))] bottom-full -translate-x-1/2',
'@2xl/media-root:[left:var(--media-slider-pointer)]',
'opacity-0 scale-80 blur-sm origin-bottom',
'transition-[scale,opacity,filter] duration-150',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:opacity-100',
'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:scale-100',
- 'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:blur-none',
- 'has-[[role=img][data-loading]]:max-h-24'
+ 'has-[[role=img]:not([data-hidden])]:group-data-pointing/slider:blur-none'
),
imageWrapper: cn(
baseThumbnail.imageWrapper,
'after:absolute after:inset-0 after:rounded-[inherit]',
'after:ring-1 after:ring-black/5 after:shadow-sm after:shadow-black/20'
),
- image: cn(baseThumbnail.image, 'max-w-(--media-slider-thumbnail-max-width)'),
+ image: cn(
+ baseThumbnail.image,
+ 'max-w-(--media-slider-thumbnail-max-width)',
+ 'max-h-(--media-slider-thumbnail-max-height)'
+ ),
};
/* ==========================================================================