feat(react): support native caption track shifting in video skins (#636)

This commit is contained in:
Sam Potts
2026-02-27 12:52:30 +11:00
committed by GitHub
parent 5f1cafaa25
commit ac346dbbd6
6 changed files with 96 additions and 30 deletions
@@ -573,16 +573,8 @@
z-index: 20;
font-size: 1rem;
text-wrap: balance;
/* The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: 600ms;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.media-minimal-skin .media-captions {
transition-duration: 50ms;
}
}
.media-minimal-skin .media-captions__container {
display: flex;
flex-direction: column;
@@ -630,8 +622,33 @@
}
}
/* Shift captions up when controls visible */
/* Caption shifting styles (custom and native) */
.media-minimal-skin {
--media-caption-track-delay: 600ms;
--media-caption-track-y: -0.5rem;
}
.media-minimal-skin:has(.media-controls[data-visible]) {
--media-caption-track-delay: 25ms;
--media-caption-track-y: -3rem;
}
.media-minimal-skin .media-captions,
.media-minimal-skin video::-webkit-media-text-track-container {
/* NOTE: The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: var(--media-caption-track-delay);
}
.media-minimal-skin video::-webkit-media-text-track-container {
transform: translateY(var(--media-caption-track-y)) scale(0.98);
z-index: 1;
font-family: inherit;
}
/* When controls are visible, shift captions up to avoid overlap */
.media-minimal-skin .media-controls[data-visible] ~ .media-captions {
transform: translateY(-2.5rem);
transition-delay: 25ms;
transform: translateY(calc(var(--media-caption-track-y) - 0.5rem));
}
@media (prefers-reduced-motion: reduce) {
.media-minimal-skin .media-captions,
.media-minimal-skin video::-webkit-media-text-track-container {
transition-duration: 50ms;
}
}
@@ -149,14 +149,27 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
'[&>img]:object-cover [&>img]:pointer-events-none',
'[&>img]:transition-opacity [&>img]:duration-[250ms]',
'[&>img:not([data-visible])]:opacity-0',
// Caption track CSS variables (consumed by native track container below)
'[--media-caption-track-delay:600ms]',
'[--media-caption-track-y:-0.5rem]',
'has-[[data-controls][data-visible]]:[--media-caption-track-delay:25ms]',
'has-[[data-controls][data-visible]]:[--media-caption-track-y:-3.5rem]',
// Native caption track container
'[&_video::-webkit-media-text-track-container]:transition-transform',
'[&_video::-webkit-media-text-track-container]:duration-150',
'[&_video::-webkit-media-text-track-container]:ease-out',
'[&_video::-webkit-media-text-track-container]:delay-(--media-caption-track-delay)',
'[&_video::-webkit-media-text-track-container]:translate-y-(--media-caption-track-y)',
'[&_video::-webkit-media-text-track-container]:scale-98',
'[&_video::-webkit-media-text-track-container]:z-1',
'[&_video::-webkit-media-text-track-container]:font-[inherit]',
'motion-reduce:[&_video::-webkit-media-text-track-container]:duration-50',
// Fullscreen
'[&:fullscreen]:rounded-none',
className
)}
{...rest}
>
{children}
<BufferingIndicator
render={(props, state) =>
state.visible ? (
@@ -203,6 +216,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
/>
<Controls.Root
data-controls="" // Used as a hook for Tailwind has-[] styles
className={cn(
// Peer marker for overlay/captions
'peer/controls',
@@ -368,6 +382,8 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
'motion-reduce:duration-100'
)}
/>
{children}
</Container>
);
}
@@ -84,8 +84,6 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
return (
<Container className={cn('media-minimal-skin', className)} {...rest}>
{children}
<BufferingIndicator
render={(props, state) =>
state.visible ? (
@@ -209,6 +207,8 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
</div> */}
<div className="media-overlay" />
{children}
</Container>
);
}
+28 -11
View File
@@ -574,16 +574,8 @@
z-index: 20;
font-size: 1rem;
text-wrap: balance;
/* The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: 600ms;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.media-default-skin .media-captions {
transition-duration: 50ms;
}
}
.media-default-skin .media-captions__container {
display: flex;
flex-direction: column;
@@ -631,8 +623,33 @@
}
}
/* Shift captions up when controls visible */
/* Caption shifting styles (custom and native) */
.media-default-skin {
--media-caption-track-delay: 600ms;
--media-caption-track-y: -0.5rem;
}
.media-default-skin:has(.media-controls[data-visible]) {
--media-caption-track-delay: 25ms;
--media-caption-track-y: -3.5rem;
}
.media-default-skin .media-captions,
.media-default-skin video::-webkit-media-text-track-container {
/* NOTE: The delay must account for the controls delay/duration */
transition: transform 150ms ease-out;
transition-delay: var(--media-caption-track-delay);
}
.media-default-skin video::-webkit-media-text-track-container {
transform: translateY(var(--media-caption-track-y)) scale(0.98);
z-index: 1;
font-family: inherit;
}
/* When controls are visible, shift captions up to avoid overlap */
.media-default-skin .media-controls[data-visible] ~ .media-captions {
transform: translateY(-3rem);
transition-delay: 25ms;
transform: translateY(calc(var(--media-caption-track-y) - 0.5rem));
}
@media (prefers-reduced-motion: reduce) {
.media-default-skin .media-captions,
.media-default-skin video::-webkit-media-text-track-container {
transition-duration: 50ms;
}
}
@@ -165,14 +165,27 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
'[&>img]:object-cover [&>img]:pointer-events-none',
'[&>img]:transition-opacity [&>img]:duration-[250ms]',
'[&>img:not([data-visible])]:opacity-0',
// Caption track CSS variables (consumed by native track container below)
'[--media-caption-track-delay:600ms]',
'[--media-caption-track-y:-0.5rem]',
'has-[[data-controls][data-visible]]:[--media-caption-track-delay:25ms]',
'has-[[data-controls][data-visible]]:[--media-caption-track-y:-3.5rem]',
// Native caption track container
'[&_video::-webkit-media-text-track-container]:transition-transform',
'[&_video::-webkit-media-text-track-container]:duration-150',
'[&_video::-webkit-media-text-track-container]:ease-out',
'[&_video::-webkit-media-text-track-container]:delay-(--media-caption-track-delay)',
'[&_video::-webkit-media-text-track-container]:translate-y-(--media-caption-track-y)',
'[&_video::-webkit-media-text-track-container]:scale-98',
'[&_video::-webkit-media-text-track-container]:z-1',
'[&_video::-webkit-media-text-track-container]:font-[inherit]',
'motion-reduce:[&_video::-webkit-media-text-track-container]:duration-50',
// Fullscreen
'[&:fullscreen]:rounded-none',
className
)}
{...rest}
>
{children}
<BufferingIndicator
render={(props, state) =>
state.visible ? (
@@ -223,6 +236,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
/>
<Controls.Root
data-controls="" // Used as a hook for Tailwind has-[] styles
className={cn(
// Peer marker for overlay/captions
'peer/controls',
@@ -374,6 +388,8 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
'motion-reduce:duration-100'
)}
/>
{children}
</Container>
);
}
+2 -2
View File
@@ -84,8 +84,6 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
return (
<Container className={cn('media-default-skin', className)} {...rest}>
{children}
<BufferingIndicator
render={(props, state) =>
state.visible ? (
@@ -203,6 +201,8 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
</div> */}
<div className="media-overlay" />
{children}
</Container>
);
}