elements
lifecycle
browser
- useAudio
- useBattery
- useBluetooth
- useBreakpoints
- useBroadcastChannel
- useBrowserLocation
- useClipboard
- useCopy
- useCssVar
- useDisplayMedia
- useDocumentEvent
- useDocumentTitle
- useDocumentVisibility
- useEventListener
- useEventSource
- useEyeDropper
- useFavicon
- useFileSystemAccess
- useFps
- useFullscreen
- useGamepad
- useGeolocation
- useMeasure
- useMediaControls
- useMediaQuery
- useMemory
- useNetwork
- useObjectUrl
- useOnline
- useOtpCredential
- usePermission
- usePictureInPicture
- usePointerLock
- usePostMessage
- useRaf
- useShare
- useSpeechRecognition
- useSpeechSynthesis
- useSticky
utilities
state
user
sensors
Now playing
useEffect at 3 AMThe Hook Brothers
0:000:00
useEffect at 3 AMThe Hook Brothers
Stale Closure BluesDependency Array
It Works on My MachineThe Reducers
Merge Conflict (feat. Git)Null & The Undefined
import { useMediaControls } from '@siberiacancode/reactuse';
import {
DownloadIcon,
HeartIcon,
MoreHorizontalIcon,
PauseIcon,
PlayIcon,
SkipBackIcon,
SkipForwardIcon
} from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/utils/lib';
const formatTime = (seconds: number) => {
if (!Number.isFinite(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${String(secs).padStart(2, '0')}`;
};
const TRACKS = [
{
id: 1,
title: 'useEffect at 3 AM',
artist: 'The Hook Brothers',
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
gradient: 'from-violet-500 to-fuchsia-700'
},
{
id: 2,
title: 'Stale Closure Blues',
artist: 'Dependency Array',
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
gradient: 'from-cyan-500 to-blue-700'
},
{
id: 3,
title: 'It Works on My Machine',
artist: 'The Reducers',
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
gradient: 'from-amber-500 to-rose-600'
},
{
id: 4,
title: 'Merge Conflict (feat. Git)',
artist: 'Null & The Undefined',
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
gradient: 'from-emerald-500 to-teal-700'
}
] as const;
const Demo = () => {
const [trackIndex, setTrackIndex] = useState(0);
const track = TRACKS[trackIndex];
const audio = useMediaControls<HTMLAudioElement>({ src: track.src, type: 'audio/mp3' });
const onPrev = () => setTrackIndex((current) => (current - 1 + TRACKS.length) % TRACKS.length);
const onNext = () => setTrackIndex((current) => (current + 1) % TRACKS.length);
const progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
const onSeek = (event: React.MouseEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
const ratio = (event.clientX - rect.left) / rect.width;
audio.seek(ratio * audio.duration);
};
return (
<section className='flex w-full max-w-xs flex-col gap-4 p-4'>
<audio ref={audio.ref} />
<div
className={cn(
'relative flex aspect-square w-full flex-col justify-between overflow-hidden rounded-2xl bg-gradient-to-br p-4 shadow-lg',
track.gradient
)}
>
<span className='text-xs font-medium tracking-[0.25em] text-white/70 uppercase'>
Now playing
</span>
<div className='absolute inset-x-0 bottom-0 flex items-center justify-center gap-4 bg-gradient-to-t from-black/50 to-transparent p-4 pt-12'>
<button
aria-label='Previous'
className='flex size-9 items-center justify-center rounded-full text-white/80 transition-colors hover:bg-white/10 hover:text-white'
data-variant='unstyled'
type='button'
onClick={onPrev}
>
<SkipBackIcon className='size-5' />
</button>
<button
aria-label={audio.playing ? 'Pause' : 'Play'}
className='rounded-full!'
type='button'
onClick={() => {
audio.changeVolume(0.2);
audio.toggle();
}}
>
{audio.playing ? (
<PauseIcon className='size-5' fill='currentColor' />
) : (
<PlayIcon className='size-5 translate-x-0.5' fill='currentColor' />
)}
</button>
<button
aria-label='Next'
className='flex size-9 items-center justify-center rounded-full text-white/80 transition-colors hover:bg-white/10 hover:text-white'
data-variant='unstyled'
type='button'
onClick={onNext}
>
<SkipForwardIcon className='size-5' />
</button>
</div>
</div>
<div className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col leading-tight'>
<span className='text-foreground truncate text-lg font-bold'>{track.title}</span>
<span className='text-muted-foreground truncate text-xs'>{track.artist}</span>
</div>
<div className='flex shrink-0 items-center gap-1'>
<button
aria-label='Like'
className='rounded-full!'
data-size='icon'
data-variant='ghost'
type='button'
>
<HeartIcon className='size-4' />
</button>
<button
aria-label='Download'
className='rounded-full!'
data-size='icon'
data-variant='ghost'
type='button'
>
<DownloadIcon className='size-4' />
</button>
</div>
</div>
<div className='flex flex-col gap-1.5'>
<div
className='group bg-muted relative h-1.5 w-full cursor-pointer rounded-full'
onClick={onSeek}
>
<div
className='bg-foreground absolute inset-y-0 left-0 rounded-full'
style={{ width: `${progress}%` }}
/>
<div
className='bg-foreground absolute top-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-0 shadow transition-opacity group-hover:opacity-100'
style={{ left: `${progress}%` }}
/>
</div>
<div className='text-muted-foreground flex justify-between font-mono text-[10px] tabular-nums'>
<span>{formatTime(audio.currentTime)}</span>
<span>{formatTime(audio.duration)}</span>
</div>
</div>
<div className='flex flex-col gap-0.5'>
{TRACKS.map((item, index) => {
const active = index === trackIndex;
return (
<div
key={item.id}
className='group hover:bg-muted/40 flex cursor-pointer gap-2 rounded-xl px-1 py-2'
onClick={() => setTrackIndex(index)}
>
<div
className={cn(
'flex size-11 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br',
item.gradient
)}
>
{active && audio.playing && (
<div className='flex items-center gap-0.5'>
<span className='h-2.5 w-0.5 animate-pulse rounded-full bg-white' />
<span className='h-1.5 w-0.5 animate-pulse rounded-full bg-white [animation-delay:150ms]' />
<span className='h-3 w-0.5 animate-pulse rounded-full bg-white [animation-delay:300ms]' />
</div>
)}
</div>
<div className='flex min-w-0 flex-1 flex-col gap-0.5 leading-tight'>
<span
className={cn(
'truncate text-sm font-medium',
active ? 'text-primary' : 'text-foreground'
)}
>
{item.title}
</span>
<span className='text-muted-foreground truncate text-xs'>{item.artist}</span>
</div>
<div className='mr-2 flex items-center'>
<MoreHorizontalIcon className='text-muted-foreground size-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-100' />
</div>
</div>
);
})}
</div>
</section>
);
};
export default Demo;
Installation
pnpm add @siberiacancode/reactuseUsage
const { playing, play, pause } = useMediaControls(videoRef, 'video.mp4');
// or
const { playing, play, pause } = useMediaControls(audioRef, { src: 'audio.mp3', type: 'audio/mp3' });
// or
const { ref, playing, play, pause } = useMediaControls<HTMLVideoElement>('video.mp4');
// or
const { ref, playing, play, pause } = useMediaControls<HTMLVideoElement>({ src: 'video.mp4', type: 'video/mp4' });Type Declarations
import type { HookTarget } from '@/utils/helpers';
import type { StateRef } from '../useRefState/useRefState';
export interface UseMediaSource {
/** The media attribute of the media */
media?: string;
/** The source URL of the media */
src: string;
/** The MIME type of the media */
type?: string;
}
export interface UseMediaControlsReturn {
/** Whether the media is currently buffering */
buffered: [number, number][];
/** The current playback position in seconds */
currentTime: number;
/** The total duration of the media in seconds */
duration: number;
/** Whether the media has ended */
ended: boolean;
/** Whether the media is currently muted */
muted: boolean;
/** The current playback rate (1.0 is normal speed) */
playbackRate: number;
/** Whether the media is currently playing */
playing: boolean;
/** Whether the media is currently seeking */
seeking: boolean;
/** Whether the media is currently stalled */
stalled: boolean;
/** The current volume level (0.0 to 1.0) */
volume: number;
/** Whether the media is currently waiting */
waiting: boolean;
/** Set the playback rate */
changePlaybackRate: (rate: number) => void;
/** Set the volume level (0.0 to 1.0) */
changeVolume: (volume: number) => void;
/** Set the muted state */
mute: () => void;
/** Pause the media */
pause: () => void;
/** Start playing the media */
play: () => Promise<void>;
/** Seek to a specific time in seconds */
seek: (time: number) => void;
/** Toggle between play and pause */
toggle: () => Promise<void>;
/** Set the unmuted state */
unmute: () => void;
}
export interface UseMediaControls {
(target: HookTarget, src: string): UseMediaControlsReturn;
(target: HookTarget, options: UseMediaSource): UseMediaControlsReturn;
<Target extends HTMLMediaElement>(
src: string
): UseMediaControlsReturn & {
ref: StateRef<Target>;
};
<Target extends HTMLMediaElement>(
options: UseMediaSource
): UseMediaControlsReturn & { ref: StateRef<Target> };
}API
Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| target | HookTarget | - | The target media element |
| src | string | - | The source URL of the media |
Returns
UseMediaControlsReturnParameters
| Name | Type | Default | Note |
|---|---|---|---|
| target | HookTarget | - | The target media element |
| options | UseMediaSource | - | The media source configuration |
Returns
UseMediaControlsReturnParameters
| Name | Type | Default | Note |
|---|---|---|---|
| src | string | - | The source URL of the media |
Returns
UseMediaControlsReturn & { ref: StateRef<Target> }Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| options | UseMediaSource | - | The media source configuration |
Returns
UseMediaControlsReturn & { ref: StateRef<Target> }Contributors
ddebabin
Last updated on