Guides
Compose your own player, play adaptive streams, write a loader and theme the controls.
Compose your own
Both players are a few lines on top of unstyled primitives. Rearrange them, restyle them, or add what you need.
$ npx shadcn@latest add https://bobine.fyi/r/media.json<MediaRoot className="aspect-video bg-media-scrim text-media-foreground">
<MediaVideo src="/clip.mp4" className="size-full object-cover" />
<MediaSurface />
<MediaControls className="absolute inset-x-3 bottom-3 flex-col gap-1">
<MediaSeek />
<div className="flex items-center gap-1">
<MediaPlayButton />
<MediaMuteButton />
<MediaVolume />
<MediaTime /> / <MediaTime type="duration" />
<span className="grow" />
<MediaPlaybackRateButton />
<MediaPictureInPictureButton />
<MediaFullscreenButton />
</div>
</MediaControls>
</MediaRoot>Streaming
Pass a loader for adaptive streams. Safari plays HLS natively. Other browsers fetch hls.js only when an .m3u8 source appears.
$ npx shadcn@latest add https://bobine.fyi/r/media-hls.json"use client"
import { VideoPlayer } from "@/components/ui/video-player"
import { hlsLoader } from "@/lib/media-hls"
<VideoPlayer src="https://example.com/stream.m3u8" loader={hlsLoader} />Custom loaders
A loader attaches a source the browser cannot play natively, such as DASH. When you pass one, bobine leaves src off the element, so the loader must set media.src itself for any source it does not handle. Return a cleanup function. It runs when src changes or the player unmounts.
import type { MediaSourceLoader } from "@/components/ui/media"
export const dashLoader: MediaSourceLoader = (media, src) => {
if (!src.endsWith(".mpd")) {
// not a DASH source: hand it to the browser
media.src = src
return () => media.removeAttribute("src")
}
const player = attachYourDashPlayer(media, src)
return () => player.destroy()
}Theming
The players read your shadcn theme tokens (background, foreground, muted, primary, border), so they follow your light and dark mode. Video controls use two more tokens that stay the same in both modes. Requires React 19 and Tailwind CSS 4.
/* globals.css: constant across light and dark */
:root {
--media-foreground: oklch(1 0 0); /* icons and text over video */
--media-scrim: oklch(0 0 0); /* video ground and control glass */
}