Customized Video Player Component
A fully-featured and accessible custom video player. It includes play/pause controls, a seekable progress bar, volume slider, fullscreen toggle, time display, and loading indicators, all wrapped in an auto-hiding interface
LTR
RTL
<div class="max-w-4xl w-full">
<style>
/* Custom styles for volume slider fill */
.volume-slider-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: #3b82f6;
border-radius: 3px;
width: 100%;
transform-origin: left;
transform: scaleX(1);
}
</style>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-6 text-center rtl:text-right">Custom Video Player</h1>
<div
x-data="{
isPlaying: false,
isMuted: false,
volume: 1,
currentTime: 0,
duration: 0,
progress: 0,
showControls: true,
isHovering: false,
showPlayPauseOverlay: false,
isLoading: true,
isFullscreen: false,
initPlayer() {
this.duration = this.$refs.video.duration;
this.isLoading = false;
this.$nextTick(() => {
setInterval(() => {
if (!this.isHovering && this.isPlaying) {
this.showControls = false;
}
}, 3000);
});
document.addEventListener('fullscreenchange', () => {
this.isFullscreen = !!document.fullscreenElement;
});
},
togglePlay() {
if (this.$refs.video.paused) {
this.$refs.video.play();
this.isPlaying = true;
} else {
this.$refs.video.pause();
this.isPlaying = false;
}
this.showPlayPauseOverlay = true;
setTimeout(() => {
this.showPlayPauseOverlay = false;
}, 500);
this.showControls = true;
},
updateProgress() {
this.currentTime = this.$refs.video.currentTime;
this.progress = (this.currentTime / this.duration) * 100;
},
seek(event) {
const rect = this.$refs.progressBar.getBoundingClientRect();
const percent = (event.clientX - rect.left) / rect.width;
this.$refs.video.currentTime = percent * this.duration;
},
toggleMute() {
this.isMuted = !this.isMuted;
this.$refs.video.muted = this.isMuted;
if (this.isMuted) {
this.$refs.video.volume = 0;
} else {
this.$refs.video.volume = this.volume;
}
},
updateVolume() {
this.$refs.video.volume = this.volume;
this.isMuted = this.volume === 0;
},
toggleFullscreen() {
if (!document.fullscreenElement) {
if (this.$refs.playerContainer.requestFullscreen) {
this.$refs.playerContainer.requestFullscreen();
} else if (this.$refs.playerContainer.webkitRequestFullscreen) {
this.$refs.playerContainer.webkitRequestFullscreen();
} else if (this.$refs.playerContainer.msRequestFullscreen) {
this.$refs.playerContainer.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
},
formatTime(seconds) {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
},
onVideoEnd() {
this.isPlaying = false;
this.showControls = true;
}
}"
x-init="initPlayer"
@keydown.window.space.prevent="togglePlay"
class="relative bg-black rounded-lg overflow-hidden shadow-2xl"
x-ref="playerContainer"
>
<!-- Video Element -->
<video
x-ref="video"
@click="togglePlay"
@timeupdate="updateProgress"
@loadedmetadata="initPlayer"
@ended="onVideoEnd"
@waiting="isLoading = true"
@canplay="isLoading = false"
class="w-full h-auto cursor-pointer"
poster="https://wavykits.com/storage/poster.png"
>
<source src="https://wavykits.com/images/wavykits_presentation.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<!-- Controls Overlay -->
<div
x-show="showControls || isHovering"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@mouseenter="isHovering = true"
@mouseleave="isHovering = false"
class="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex flex-col justify-end p-4"
>
<!-- Progress Bar -->
<div class="w-full mb-3">
<div
x-ref="progressBar"
@click="seek($event)"
class="relative w-full h-2 bg-gray-700 rounded-full overflow-hidden cursor-pointer group"
>
<div
class="absolute top-0 left-0 h-full bg-blue-500 rounded-full"
:style="`width: ${progress}%`"
></div>
<div
class="absolute top-1/2 right-0 w-3 h-3 bg-blue-500 rounded-full transform translate-x-1/2 -translate-y-1/2 shadow-md opacity-0 group-hover:opacity-100"
:style="`left: ${progress}%`"
></div>
</div>
</div>
<!-- Controls -->
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4 rtl:space-x-reverse">
<!-- Play/Pause Button -->
<button
@click="togglePlay"
class="text-white hover:text-blue-400 transition-colors"
:class="{ 'text-blue-400': isPlaying }"
>
<!-- Play Icon -->
<svg x-show="!isPlaying" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
<!-- Pause Icon -->
<svg x-show="isPlaying" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" style="display: none;">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
</button>
<!-- Volume Control -->
<div class="flex items-center space-x-2 rtl:space-x-reverse">
<button
@click="toggleMute"
class="text-white hover:text-blue-400 transition-colors"
:class="{ 'text-blue-400': isMuted }"
>
<!-- Volume On Icon -->
<svg x-show="!isMuted && volume > 0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z" />
</svg>
<!-- Volume Mute Icon -->
<svg x-show="isMuted || volume == 0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" style="display: none;">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75L19.5 12m0 0l2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z" />
</svg>
</button>
<div class="relative w-[100px] h-[6px] bg-gray-700 rounded-full overflow-hidden">
<div
class="volume-slider-fill"
:style="`transform: scaleX(${volume})`"
></div>
<div
class="absolute top-1/2 right-0 w-3 h-3 bg-blue-500 rounded-full transform translate-x-1/2 -translate-y-1/2 shadow-md"
:style="`right: ${(1 - volume) * 100}%`"
></div>
<input
type="range"
min="0"
max="1"
step="0.01"
x-model="volume"
@input="updateVolume"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer z-10"
>
</div>
</div>
<!-- Time Display -->
<div class="text-white text-sm">
<span x-text="formatTime(currentTime)"></span> /
<span x-text="formatTime(duration)"></span>
</div>
</div>
<!-- Fullscreen Button -->
<button
@click="toggleFullscreen"
class="text-white hover:text-blue-400 transition-colors"
>
<!-- Expand Icon -->
<svg x-show="!isFullscreen" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>
<!-- Compress Icon -->
<svg x-show="isFullscreen" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" style="display: none;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5M15 15l5.25 5.25" />
</svg>
</button>
</div>
</div>
<!-- Play/Pause Overlay -->
<div
x-show="showPlayPauseOverlay"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-75"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-125"
class="absolute inset-0 flex items-center justify-center pointer-events-none"
>
<div class="bg-black/50 rounded-full w-28 h-28 flex items-center justify-center">
<!-- Play Icon (Solid) -->
<svg x-show="!isPlaying" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-12 h-12 text-white">
<path fill-rule="evenodd" d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.648c1.295.742 1.295 2.545 0 3.286L7.279 20.99c-1.25.717-2.779-.217-2.779-1.643V5.653z" clip-rule="evenodd" />
</svg>
<!-- Pause Icon (Solid) -->
<svg x-show="isPlaying" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-12 h-12 text-white" style="display: none;">
<path fill-rule="evenodd" d="M6.75 5.25a.75.75 0 00-.75.75v12a.75.75 0 00.75.75h.75a.75.75 0 00.75-.75V6a.75.75 0 00-.75-.75H6.75zm5.25 0a.75.75 0 00-.75.75v12a.75.75 0 00.75.75h.75a.75.75 0 00.75-.75V6a.75.75 0 00-.75-.75h-.75z" clip-rule="evenodd" />
</svg>
</div>
</div>
<!-- Loading Spinner -->
<div x-show="isLoading" class="absolute inset-0 flex items-center justify-center bg-black/50">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div>
</div>
<div class="mt-6 text-center text-gray-400 dark:text-gray-400">
<p>Click anywhere on the video to play/pause. Use spacebar to toggle play/pause.</p>
<p class="mt-2">Click on the progress bar to seek to a specific time.</p>
<p class="mt-2">Video: Wavykits Presentation</p>
</div>
</div>