Custom Video Player in React with TypeScript

Custom video player for MDX posts.

Why Default Video Players Kinda Suck (And How I Fixed It)

Let's be real: the default HTML <video> controls are ugly as hell.

You know what I'm talking about — that janky native player that looks like it crawled out of 2008. Different browsers, different looks, zero consistency with your carefully crafted design system.

Clean af, right? Controls appear on hover like proper hover magic should work.

Why Bother Building Your Own?

Because default video controls are the visual equivalent of Comic Sans.

Seriously though, native <video> controls are inconsistent between browsers, ugly by default, and you can't style them without wanting to throw your laptop out the window.

Here's why I said "screw it" and rolled my own:

  • Your design system matters — why break the vibe with browser defaults?
  • UX that doesn't suck — hover states, smooth transitions, zero jank
  • Only the features you need — no weird volume sliders or picture-in-picture buttons cluttering up the works

What I Built Instead

Zero boilerplate, maximum smooth.

The Foundation

tsx
interface VideoPlayerProps {
  src: string;
  poster?: string;
  className?: string;
}
 
export function VideoPlayer({ src, poster, className }: VideoPlayerProps) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  // ...other state
}

Standard React stuff. Nothing crazy here.

Event Listeners (The Boring But Necessary Part)

tsx
useEffect(() => {
  const video = videoRef.current;
  if (!video) return;
 
  const updateTime = () => setCurrentTime(video.currentTime);
  const updateDuration = () => setDuration(video.duration);
 
  video.addEventListener("timeupdate", updateTime);
  video.addEventListener("loadedmetadata", updateDuration);
  video.addEventListener("ended", () => setIsPlaying(false));
 
  return () => {
    video.removeEventListener("timeupdate", updateTime);
    video.removeEventListener("loadedmetadata", updateDuration);
    video.removeEventListener("ended", () => setIsPlaying(false));
  };
}, []);

Classic useEffect cleanup. Don't be that dev who forgets to remove event listeners.

Progress Bar That Actually Works

tsx
const handleProgressChange = (value: number[]) => {
  const video = videoRef.current;
  if (!video) return;
 
  const newTime = (value[0] / 100) * duration;
  video.currentTime = newTime;
  setCurrentTime(newTime);
};
 
const progressPercentage = duration ? (currentTime / duration) * 100 : 0;

Math is math. Nothing groundbreaking, but it works like butter.

The Hover Magic

tsx
<div
  className="relative w-full max-w-4xl mx-auto bg-black rounded-lg overflow-hidden group"
  onMouseEnter={() => setShowControls(true)}
  onMouseLeave={() => setShowControls(false)}
>
  <div
    className={`absolute bottom-4 left-4 right-4 bg-muted/50 backdrop-blur-sm rounded-full px-2 py-0.5 transition-opacity duration-300 ${
      showControls ? "opacity-100" : "opacity-0"
    }`}
  >
  </div>
</div>

That backdrop blur with the fade transition? Chef's kiss. Hella smooth.

The MDX Hack That Made Me Feel Clever

Here's the sneaky part — I hijacked the img component in MDX to auto-detect video files:

tsx
img: ({ src, alt, width, height, ...props }) => {
  const srcString = typeof src === 'string' ? src : '';
  
  // Check if it's a video file
  if (srcString.match(/\.(mp4|webm|ogg|mov|avi)(\?.*)?$/i)) {
    return <VideoPlayer src={srcString} />;
  }
  
  // Regular image handling...
}

Now I can just write ![](video.mp4) in my MDX and boom — custom video player.

No special syntax, no weird imports. Just works.

Wrap It Up

Default video players are trash, custom ones are clean as hell.

The whole thing took maybe an afternoon to build, and now every video on my site looks consistent instead of like some browser threw up on my design.

Steal this idea. Your users will thank you, and you won't have to explain to your designer why that one video looks like it belongs on a different website.

Future me might add keyboard shortcuts or picture-in-picture, but honestly? This does everything I need without any of the jank.

Command Palette

Search for a command to run...