feat(react): add playback rate button component (#639)

This commit is contained in:
Sam Potts
2026-02-27 17:05:41 +11:00
committed by GitHub
parent ac346dbbd6
commit 3305fb3db6
8 changed files with 162 additions and 8 deletions
@@ -346,7 +346,9 @@
/* Icon button variant */
.media-minimal-skin .media-button--icon {
display: grid;
padding: 0.625rem;
width: 2.375rem;
padding: 0;
aspect-ratio: 1;
background: transparent;
color: oklch(1 0 0);
}
@@ -367,6 +369,15 @@
}
}
/* Playback rate button */
.media-minimal-skin .media-button--playback-rate {
padding: 0;
}
.media-minimal-skin .media-button--playback-rate span {
width: 4ch;
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Icons
========================================================================== */
@@ -375,6 +386,7 @@
position: relative;
}
.media-minimal-skin .media-icon {
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
@@ -25,6 +25,7 @@ import { FullscreenButton } from '@/ui/fullscreen-button';
import { MuteButton } from '@/ui/mute-button';
import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { SeekButton } from '@/ui/seek-button';
import { Time } from '@/ui/time';
import type { MinimalVideoSkinProps } from './minimal-skin';
@@ -53,14 +54,14 @@ const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { varian
type="button"
className={cn(
// Shared
'shrink-0 border-none cursor-pointer select-none',
'items-center justify-center shrink-0 border-none cursor-pointer select-none',
'outline-2 outline-transparent -outline-offset-2',
'transition-[background-color,color,outline-offset] duration-150 ease-out',
'disabled:cursor-not-allowed disabled:opacity-50 disabled:grayscale',
// Variant
variant === 'icon'
? cn(
'grid p-2.5 bg-transparent rounded-md',
'grid w-[2.375rem] aspect-square bg-transparent rounded-md',
'text-white',
'hover:text-white/80 hover:no-underline',
'focus-visible:text-white/80',
@@ -68,7 +69,7 @@ const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { varian
'aria-expanded:text-white/80'
)
: cn(
'flex items-center justify-center py-2 px-4 bg-white rounded-lg',
'flex py-2 px-4 bg-white rounded-lg',
'text-black font-medium',
'focus-visible:outline-white focus-visible:outline-offset-2'
),
@@ -297,6 +298,14 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
</span>
<span className={cn('flex items-center gap-[0.075rem]', '@2xl/media-root:gap-0.5')}>
<PlaybackRateButton
render={(props, state) => (
<Button variant="icon" {...props} className="p-0">
<span className="w-[4ch] tabular-nums">{state.rate}&times;</span>
</Button>
)}
/>
<MuteButton
render={(props, state) => (
<Button variant="icon" {...props}>
@@ -25,6 +25,7 @@ import { FullscreenButton } from '@/ui/fullscreen-button';
import { MuteButton } from '@/ui/mute-button';
import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { SeekButton } from '@/ui/seek-button';
import { Time } from '@/ui/time';
import type { BaseSkinProps } from '../types';
@@ -163,6 +164,14 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
</span>
<span className="media-button-group">
<PlaybackRateButton
render={(props, state) => (
<Button {...props} className="media-button--icon media-button--playback-rate">
<span>{state.rate}&times;</span>
</Button>
)}
/>
<MuteButton
render={(props, state) => (
<Button {...props} className="media-button--icon">
+13 -1
View File
@@ -358,7 +358,9 @@
/* Icon button variant */
.media-default-skin .media-button--icon {
display: grid;
padding: 0.5rem;
width: 2.125rem;
padding: 0;
aspect-ratio: 1;
background: transparent;
color: oklch(1 0 0 / 0.9);
text-shadow: 0 1px 0 oklch(0 0 0 / 0.25);
@@ -381,6 +383,15 @@
}
}
/* Playback rate button */
.media-default-skin .media-button--playback-rate {
padding: 0;
}
.media-default-skin .media-button--playback-rate span {
width: 4ch;
font-variant-numeric: tabular-nums;
}
/* ==========================================================================
Icons
========================================================================== */
@@ -389,6 +400,7 @@
position: relative;
}
.media-default-skin .media-icon {
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
@@ -25,6 +25,7 @@ import { FullscreenButton } from '@/ui/fullscreen-button';
import { MuteButton } from '@/ui/mute-button';
import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { SeekButton } from '@/ui/seek-button';
import { Time } from '@/ui/time';
import type { VideoSkinProps } from './skin';
@@ -66,14 +67,14 @@ const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { varian
type="button"
className={cn(
// Shared
'shrink-0 border-none cursor-pointer select-none',
'items-center justify-center shrink-0 border-none cursor-pointer select-none',
'outline-2 outline-transparent -outline-offset-2',
'transition-[background-color,color,outline-offset] duration-150 ease-out',
'disabled:cursor-not-allowed disabled:opacity-50 disabled:grayscale',
// Variant
variant === 'icon'
? cn(
'grid p-2 bg-transparent rounded-full',
'grid w-[2.125rem] aspect-square bg-transparent rounded-full',
'text-white/90',
'text-shadow-2xs text-shadow-black/25',
'hover:bg-white/10 hover:text-white hover:no-underline',
@@ -82,7 +83,7 @@ const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'> & { varian
'aria-expanded:bg-white/10 aria-expanded:text-white'
)
: cn(
'flex items-center justify-center py-2 px-4 bg-white rounded-full',
'flex py-2 px-4 bg-white rounded-full',
'text-black font-medium',
'focus-visible:outline-blue-500 focus-visible:outline-offset-2'
),
@@ -304,6 +305,14 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Time.Value type="duration" className="text-shadow-2xs text-shadow-black/25 tabular-nums" />
</Time.Group>
<PlaybackRateButton
render={(props, state) => (
<Button variant="icon" {...props}>
<span className="w-[4ch] tabular-nums">{state.rate}&times;</span>
</Button>
)}
/>
<MuteButton
render={(props, state) => (
<Button variant="icon" {...props}>
@@ -25,6 +25,7 @@ import { FullscreenButton } from '@/ui/fullscreen-button';
import { MuteButton } from '@/ui/mute-button';
import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { SeekButton } from '@/ui/seek-button';
import { Time } from '@/ui/time';
import type { BaseSkinProps } from '../types';
@@ -158,6 +159,14 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Time.Value type="duration" className="media-time__value" />
</Time.Group>
<PlaybackRateButton
render={(props, state) => (
<Button {...props} className="media-button--icon media-button--playback-rate">
<span>{state.rate}&times;</span>
</Button>
)}
/>
<MuteButton
render={(props, state) => (
<Button {...props} className="media-button--icon">