Video
video ui Video player wrapper with a custom controls overlay on a native video element. Supports src, poster, autoplay, loop, muted, custom or native controls, playback rate, fullscreen, time display, a progress bar, play/pause, skip, and volume control.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/video.json $ npx shadcn@latest add https://uipkge.dev/r/react/video.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/video.json $ bunx shadcn@latest add https://uipkge.dev/r/react/video.json Named registry:
npx shadcn@latest add @uipkge-react/video Installs to: components/ui/video/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
src Video source URL. | string | — | required |
poster Poster image shown before playback. | string | — | optional |
autoplay Autoplay on mount. Note: browsers may block autoplay with sound. | boolean | — | optional |
loop Loop playback. | boolean | — | optional |
muted Muted audio. | boolean | — | optional |
nativeControls Use native browser controls instead of the custom overlay. Default false. | boolean | — | optional |
playbackRate Initial playback rate. Default 1. | number | — | optional |
aspectRatio Aspect ratio class or inline style value. Default '16/9'. | string | — | optional |
npm dependencies
Files installed (2)
-
components/ui/video/Video.tsx 9.8 kB
import * as React from 'react' import { Maximize, Minimize, Pause, Play, RotateCcw, RotateCw, Volume2, VolumeX } from 'lucide-react' import { cn } from '@/lib/utils' export interface VideoProps extends React.HTMLAttributes<HTMLDivElement> { /** Video source URL. */ src: string /** Poster image shown before playback. */ poster?: string /** Autoplay on mount. Note: browsers may block autoplay with sound. */ autoplay?: boolean /** Loop playback. */ loop?: boolean /** Muted audio. */ muted?: boolean /** Use native browser controls instead of the custom overlay. Default false. */ nativeControls?: boolean /** Initial playback rate. Default 1. */ playbackRate?: number /** Aspect ratio class or inline style value. Default '16/9'. */ aspectRatio?: string } const Video = React.forwardRef<HTMLDivElement, VideoProps>( ( { src, poster, autoplay = false, loop = false, muted = false, nativeControls = false, playbackRate = 1, aspectRatio = '16/9', className, ...props }, ref, ) => { const videoRef = React.useRef<HTMLVideoElement | null>(null) const containerRef = React.useRef<HTMLDivElement | null>(null) const [playing, setPlaying] = React.useState(false) const [current, setCurrent] = React.useState(0) const [duration, setDuration] = React.useState(0) const [volume, setVolume] = React.useState(muted ? 0 : 1) const [isMuted, setIsMuted] = React.useState(muted) const [isFullscreen, setIsFullscreen] = React.useState(false) const [showControls, setShowControls] = React.useState(true) const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null) React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement) const togglePlay = React.useCallback(() => { const v = videoRef.current if (!v) return if (v.paused) v.play() else v.pause() }, []) const onPlay = React.useCallback(() => setPlaying(true), []) const onPause = React.useCallback(() => setPlaying(false), []) const onTimeUpdate = React.useCallback(() => { const v = videoRef.current if (!v) return setCurrent(v.currentTime) }, []) const onLoadedMetadata = React.useCallback(() => { const v = videoRef.current if (!v) return setDuration(v.duration || 0) v.playbackRate = playbackRate v.volume = volume }, [playbackRate, volume]) const onVolumeChange = React.useCallback(() => { const v = videoRef.current if (!v) return setVolume(v.volume) setIsMuted(v.muted) }, []) const seek = (e: React.ChangeEvent<HTMLInputElement>) => { const v = videoRef.current if (!v) return v.currentTime = Number(e.target.value) } const setVolumeInput = (e: React.ChangeEvent<HTMLInputElement>) => { const v = videoRef.current if (!v) return v.volume = Number(e.target.value) v.muted = Number(e.target.value) === 0 } const toggleMute = () => { const v = videoRef.current if (!v) return v.muted = !v.muted } const skip = (seconds: number) => { const v = videoRef.current if (!v) return v.currentTime = Math.min(Math.max(v.currentTime + seconds, 0), v.duration || 0) } const toggleFullscreen = () => { const el = containerRef.current if (!el) return if (document.fullscreenElement) { document.exitFullscreen() } else { el.requestFullscreen() } } const onFullscreenChange = React.useCallback(() => { setIsFullscreen(!!document.fullscreenElement) }, []) const onMouseMove = () => { setShowControls(true) if (hideTimerRef.current) clearTimeout(hideTimerRef.current) hideTimerRef.current = setTimeout(() => { setPlaying((p) => { if (p) setShowControls(false) return p }) }, 2500) } const onMouseLeave = () => { if (hideTimerRef.current) clearTimeout(hideTimerRef.current) if (videoRef.current && !videoRef.current.paused) setShowControls(false) } const formatTime = (s: number): string => { if (!s || !isFinite(s)) return '0:00' const m = Math.floor(s / 60) const sec = Math.floor(s % 60) return `${m}:${sec.toString().padStart(2, '0')}` } const progress = duration > 0 ? (current / duration) * 100 : 0 React.useEffect(() => { document.addEventListener('fullscreenchange', onFullscreenChange) return () => { document.removeEventListener('fullscreenchange', onFullscreenChange) if (hideTimerRef.current) clearTimeout(hideTimerRef.current) } }, [onFullscreenChange]) React.useEffect(() => { const v = videoRef.current if (v) v.playbackRate = playbackRate }, [playbackRate]) React.useEffect(() => { const v = videoRef.current if (v) v.muted = muted }, [muted]) return ( <div ref={containerRef} data-uipkge="" data-slot="video" className={cn('group relative overflow-hidden rounded-lg bg-black', className)} style={{ aspectRatio }} onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} {...props} > <video ref={videoRef} data-slot="video-element" className="size-full object-contain" src={src} poster={poster} autoPlay={autoplay} loop={loop} muted={muted} controls={nativeControls} playsInline onPlay={onPlay} onPause={onPause} onTimeUpdate={onTimeUpdate} onLoadedMetadata={onLoadedMetadata} onVolumeChange={onVolumeChange} onClick={togglePlay} /> {!nativeControls && ( <div data-slot="video-controls" className={cn( 'absolute inset-0 flex flex-col justify-between transition-opacity duration-200', showControls || !playing ? 'opacity-100' : 'opacity-0', )} > {/* Top spacer / click target */} <div className="flex-1" onClick={togglePlay} /> {/* Bottom controls bar */} <div className="bg-gradient-to-t from-black/80 to-transparent px-3 pt-6 pb-2"> {/* Progress bar */} <div className="mb-2 flex items-center gap-2"> <span className="text-xs text-white/80 tabular-nums">{formatTime(current)}</span> <input type="range" min="0" max={duration || 0} step="0.1" value={current} className="accent-primary h-1 flex-1 cursor-pointer appearance-none rounded-full bg-white/30 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary" onChange={seek} /> <span className="text-xs text-white/80 tabular-nums">{formatTime(duration)}</span> </div> {/* Buttons row */} <div className="flex items-center gap-2"> <button type="button" className="text-white/90 transition-colors hover:text-white" aria-label={playing ? 'Pause' : 'Play'} onClick={togglePlay} > {playing ? <Pause className="size-5" /> : <Play className="size-5" />} </button> <button type="button" className="text-white/90 transition-colors hover:text-white" aria-label="Rewind 10s" onClick={() => skip(-10)} > <RotateCcw className="size-4" /> </button> <button type="button" className="text-white/90 transition-colors hover:text-white" aria-label="Forward 10s" onClick={() => skip(10)} > <RotateCw className="size-4" /> </button> {/* Volume */} <div className="group/volume flex items-center gap-1.5"> <button type="button" className="text-white/90 transition-colors hover:text-white" aria-label={isMuted ? 'Unmute' : 'Mute'} onClick={toggleMute} > {isMuted || volume === 0 ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />} </button> <input type="range" min="0" max="1" step="0.05" value={isMuted ? 0 : volume} className="accent-primary h-1 w-0 cursor-pointer appearance-none rounded-full bg-white/30 transition-all duration-200 group-hover/volume:w-16 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary" onChange={setVolumeInput} /> </div> <div className="flex-1" /> <button type="button" className="text-white/90 transition-colors hover:text-white" aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} onClick={toggleFullscreen} > {isFullscreen ? <Minimize className="size-4" /> : <Maximize className="size-4" />} </button> </div> </div> </div> )} </div> ) }, ) Video.displayName = 'Video' export { Video } -
components/ui/video/index.ts 0 kB
export { Video, type VideoProps } from './Video'
Raw manifest: https://uipkge.dev/r/react/video.json