A video component.
Here's the <Audio />
component in action.
Be careful, while an image retains its width
and height
attributes after it loads, a video will override these attributes with its own dimensions once it loads.
If the width
and height
attributes are not the same as the video's dimensions, this will cause a layout shift. This is why the video component sets the width
and height
attributes of the video element to the provided values while the video is loading, and then removes them once the video is ready.
Make sure to either set the width
and height
attributes to the video's original resolution or style it with custom dimensions (e.g. w-[800px] h-[450px]
). It's usually a good idea to respect the video's original ratio, otherwise you'll get windowboxing.
import { forwardRef, useEffect, useImperativeHandle, useRef, useState, type VideoHTMLAttributes } from 'react'import { cn } from '#app/utils/tailwind-merge'
export type VideoProps = VideoHTMLAttributes<HTMLVideoElement> & { src: string width: string}/* Re-do this component - it's just here so the audio route doesn't error out */const Video = forwardRef<HTMLVideoElement, VideoProps>(({ src, width, height, className, ...props }, ref) => { const [state, setState] = useState<'loading' | 'ready' | 'failed'>('loading') const videoRef = useRef<HTMLVideoElement | null>(null) useImperativeHandle<HTMLVideoElement | null, HTMLVideoElement | null>(ref, () => videoRef.current) // Merge the two refs
useEffect(() => { if (videoRef.current) { if (videoRef.current.networkState === HTMLMediaElement.NETWORK_NO_SOURCE || videoRef.current.error) { onError() } } }, [videoRef])
const onReady = () => { setState('ready') } const onError = () => { setState('failed') }
return ( <video ref={videoRef} src={src} width={width} height={height} onCanPlay={onReady} onError={onError} data-loading={state === 'loading' || undefined} data-ready={state === 'ready' || undefined} data-failed={state === 'failed' || undefined} {...props} className={cn('video', className)} /> )})Video.displayName = 'Video'export { Video }