Skip to content
    Style: Wireframe

    Components · Media

    Video player

    A poster with one Play button; native controls and captions once it plays. Nothing plays by itself.

    1.2% of the 2,073 sites in The UI Index have a media player (24 sites).

    Used in UI Pro: Media

    A tour of the themes.dev UI docs0:06

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/video-player.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/video-player.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS and the script.

    video-player.client.ts
    // Video player behaviour, shared by the Astro, React and HTML versions: the Play button starts the video, shows the
    // native controls, moves focus to the video and removes itself.
    declare global {
      interface Window {
        __caseroomVideo?: boolean;
      }
    }
    
    if (typeof window !== 'undefined' && !window.__caseroomVideo) {
      window.__caseroomVideo = true;
      document.addEventListener('click', (e) => {
        const b = (e.target as Element).closest<HTMLButtonElement>('[data-video-play]');
        if (!b) return;
        const v = b.parentElement?.querySelector('video');
        if (!v) return;
        v.controls = true;
        b.remove();
        v.focus();
        void v.play().catch(() => {});
      });
    }
    
    export {};
    video-player.tsx
    import { cn } from '@/lib/utils';
    import './video-player.client';
    import { PLAY_PATH, video as c } from './video-player.variants';
    
    export interface VideoPlayerProps {
      src: string;
      poster: string;
      title: string;
      /** e.g. "0:06" */
      duration?: string;
      /** A WebVTT captions file. */
      captions?: string;
      className?: string;
    }
    
    /** A poster with one Play button; pressing it plays the video with native controls and captions. */
    export function VideoPlayer({ src, poster, title, duration, captions, className }: VideoPlayerProps) {
      return (
        <figure className={cn(c.root, className)}>
          <div className={c.frame}>
            <video src={src} poster={poster} preload="none" playsInline aria-label={title} className={c.media}>
              {captions && <track kind="captions" src={captions} srcLang="en" label="English" default />}
            </video>
            <button type="button" data-video-play="" aria-label={`Play video: ${title}`} className={c.play}>
              <span className={c.disc}><svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d={PLAY_PATH} /></svg></span>
            </button>
          </div>
          <figcaption className={c.caption}>
            <span className={c.title}>{title}</span>
            {duration && <span className={c.meta}>{duration}</span>}
          </figcaption>
        </figure>
      );
    }
    video-player.variants.ts
    /** A video player on native <video>: a poster with one large Play button (named after the video); pressing it
     *  starts the video with the browser's own controls and the captions track. Nothing plays by itself. */
    export const video = {
      root: 'w-full',
      frame: 'relative aspect-video overflow-hidden rounded-xl border bg-muted',
      media: 'size-full object-cover',
      play:
        // The ring sits inside the frame (the frame clips anything outside it).
        'group absolute inset-0 grid cursor-pointer place-items-center rounded-xl bg-black/20 outline-offset-[-4px] transition-colors hover:bg-black/30 active:bg-black/40 [&_svg]:size-6',
      disc: 'grid size-16 place-items-center rounded-full bg-background text-foreground shadow-lg transition-transform group-hover:scale-105 group-active:scale-95 motion-reduce:transition-none',
      caption: 'mt-3 flex items-baseline justify-between gap-4 text-sm',
      title: 'font-medium',
      meta: 'text-muted-foreground tabular-nums',
    };
    export const PLAY_PATH = 'M5.5 3.5v9l7-4.5z';