# 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.

```bash
npx shadcn@latest add https://bobine.fyi/r/media.json
```

```tsx
<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.

```bash
npx shadcn@latest add https://bobine.fyi/r/media-hls.json
```

```tsx
"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.

```ts
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.

```css
/* 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 */
}
```

## More docs

- [Getting started](https://bobine.fyi/docs.md): Install bobine and swap your first video or audio element.
- [Reference](https://bobine.fyi/docs/reference.md): Every prop, part and keyboard shortcut.
