Skip to content

React

React hooks and components for Vide. Import from @videts/vide/react.

sh
npm install @videts/vide react react-dom

Quick Start

No UI

Core only — no visual controls. Build everything yourself or use your own components.

tsx
import { useVidePlayer, Vide } from "@videts/vide/react";

function Player() {
  const player = useVidePlayer();

  return (
    <Vide.Root player={player}>
      <Vide.Video src="video.mp4" />
    </Vide.Root>
  );
}

Headless UI

UI components with behavior — no default styling. Bring your own CSS.

tsx
import { useVidePlayer, useHls, Vide } from "@videts/vide/react";

function Player() {
  const player = useVidePlayer();
  useHls(player);

  return (
    <Vide.Root player={player}>
      <Vide.UI>
        <Vide.Video src="stream.m3u8" />
        <Vide.Controls>
          <Vide.PlayButton className="my-play-btn" />
          <Vide.Progress className="my-progress" />
          <Vide.TimeDisplay className="my-time" />
          <Vide.Volume className="my-volume" />
          <Vide.FullscreenButton className="my-fs" />
        </Vide.Controls>
      </Vide.UI>
    </Vide.Root>
  );
}

Components render with BEM classes (vide-play, vide-progress, …) but no visual styles.

Themed

Headless components + the default skin. Add one CSS import.

tsx
import { useVidePlayer, useHls, Vide } from "@videts/vide/react";
import "@videts/vide/ui/theme.css"; // ← this line

function Player() {
  const player = useVidePlayer();
  useHls(player);

  return (
    <Vide.Root player={player}>
      <Vide.UI>
        <Vide.Video src="stream.m3u8" />
        <Vide.Controls>
          <Vide.PlayButton />
          <Vide.Progress />
          <Vide.TimeDisplay />
          <Vide.Volume />
          <Vide.FullscreenButton />
        </Vide.Controls>
      </Vide.UI>
    </Vide.Root>
  );
}

Hooks

useVidePlayer()

Creates and manages a player instance.

tsx
const player = useVidePlayer();
  • player.currentPlayer | null. null until the video element mounts.
  • Pass player to <Vide.Root player={player}>. Ref wiring is automatic.
  • Calls player.destroy() automatically on unmount.

useHls / useDash / useDrm / useVast / useVmap / useSsai / useUi

Plugin hooks. Call plugin.setup() when player becomes available, clean up on unmount.

tsx
useHls(player);
useHls(player, { hlsConfig: { maxBufferLength: 30 } });

useDash(player);
useDrm(player, { widevine: { licenseUrl: "..." } });
useVast(player, { tagUrl: "https://..." });
useVmap(player, { url: "https://..." });
useSsai(player);
useUi(player, { container: containerRef.current! });

All hooks are safe to call before the video element mounts (player.current is null).

useUi vs <Vide.UI>useUi is a hook that mounts the vanilla JS ui() plugin (which auto-generates DOM) onto React's lifecycle. When building UI with React components like <Vide.UI> + <Vide.PlayButton>, useUi is not needed. Using both at the same time will generate duplicate controls.

  • Build UI with React components → use <Vide.UI> (recommended)
  • Use the vanilla UI plugin as-is → use useUi

useVideEvent(player, event, handler)

Subscribe to player events with automatic cleanup.

tsx
useVideEvent(player, "statechange", ({ from, to }) => {
  console.log(`${from} → ${to}`);
});

useVideEvent(player, "ad:start", ({ adId }) => {
  console.log("Ad started:", adId);
});
  • Handler changes do not cause re-subscription (uses ref internally).
  • Unsubscribes on unmount or when player/event changes.

Components

Vide.Root

Context provider. All Vide components must be children of Vide.Root.

tsx
<Vide.Root player={player}>
  <Vide.UI>
    <Vide.Video src="video.mp4" />
    <Vide.Controls>
      <Vide.PlayButton />
    </Vide.Controls>
  </Vide.UI>
</Vide.Root>
  • player — the handle returned by useVidePlayer().

Vide.UI

Container <div> with class vide-ui. Wraps both the video element and controls. Always renders (so <Vide.Video> can mount). Manages player state classes (vide-ui--playing, vide-ui--paused, etc.) for theme.css integration.

tsx
<Vide.UI>
  <Vide.Video src="video.mp4" />
  <Vide.Controls>...</Vide.Controls>
</Vide.UI>
  • All standard <div> HTML attributes are passed through.
  • className is appended after vide-ui.

Vide.Video

Renders a <video> element and binds the player to it. Must be inside <Vide.Root>.

tsx
<Vide.Video src="video.mp4" poster="thumb.jpg" />
  • All standard <video> HTML attributes (src, poster, muted, autoPlay, etc.) are passed through.

Vide.Controls

Container <div> with class vide-controls. Renders only after the player is ready. Place UI components inside.

tsx
<Vide.Controls>
  <Vide.PlayButton />
  <Vide.Progress />
  <Vide.TimeDisplay />
  <Vide.Volume />
  <Vide.FullscreenButton />
</Vide.Controls>
  • All standard <div> HTML attributes are passed through.
  • className is appended after vide-controls.

Plugin Components

Render nothing (null). Use for conditional plugin activation. Place as children of <Vide.Root>.

tsx
<Vide.Root player={player}>
  <Vide.HlsPlugin />
  {showAds && <Vide.VastPlugin tagUrl="https://..." />}
  <Vide.UI>
    <Vide.Video src="stream.m3u8" />
    <Vide.Controls>...</Vide.Controls>
  </Vide.UI>
</Vide.Root>

Available: HlsPlugin, DashPlugin, DrmPlugin, VastPlugin, VmapPlugin, SsaiPlugin.

UI Components

Interactive controls that subscribe to player events via context. Place inside <Vide.Controls>.

Each component has a default CSS class matching the vanilla UI plugin (vide-play, vide-progress, etc.), so theme.css styles apply automatically.

tsx
<Vide.Controls>
  <Vide.PlayButton />
  <Vide.Progress />
  <Vide.Volume />
  <Vide.TimeDisplay />
  <Vide.FullscreenButton />
  <Vide.MuteButton />
</Vide.Controls>
ComponentDefault ClassPropsState Attributes
PlayButtonvide-playclassName, childrendata-playing
MuteButtonvide-muteclassName, childrendata-muted
Progressvide-progressclassNamedata-disabled, --vide-progress, --vide-progress-buffered
Volumevide-volumeclassName, childrendata-muted, --vide-volume
FullscreenButtonvide-fullscreenclassName, children, targetdata-fullscreen
TimeDisplayvide-timeclassName, separator
  • children — custom icons or content (button components).
  • CSS custom properties — use for styling sliders (same as Vide UI plugin theme).
  • data-* attributes — use for state-based CSS selectors.
  • className is appended after the default class, not replacing it.

Ad Components

Components for ad CTA, controls, and overlay during ad playback. Use with useVast() or useVmap(). Each component auto-subscribes to ad events via context and renders only during active ads.

tsx
<Vide.AdLabel />
<Vide.AdCountdown />
<Vide.AdSkip />
<Vide.AdLearnMore />
ComponentDefault ClassProps
AdLabelvide-ad-labelclassName, children
AdCountdownvide-ad-countdownclassName, format
AdSkipvide-skipclassName, children
AdLearnMorevide-ad-ctaclassName, children
AdOverlayvide-ad-overlayclassName, children

useAdState(player)

Low-level hook for custom ad UI. Returns { active, meta }.

tsx
const player = useVideContext();
const { active, meta } = useAdState(player);

if (active && meta?.clickThrough) {
  // render custom CTA
}
  • activeboolean, true during ad playback.
  • metaAdMeta | null with adId, clickThrough, skipOffset, duration, adTitle.

Styling

Every component accepts className. Your classes are appended after the default class (vide-play, vide-progress, etc.), so you can use Tailwind or any CSS framework alongside theme.css.

tsx
<Vide.Controls className="flex items-center gap-2 px-4">
  <Vide.PlayButton className="rounded-full bg-white/80 p-2 hover:bg-white" />
  <Vide.Progress className="flex-1" />
  <Vide.TimeDisplay className="text-sm text-white tabular-nums" />
  <Vide.MuteButton className="opacity-70 hover:opacity-100" />
  <Vide.FullscreenButton className="ml-auto" />
</Vide.Controls>

To go fully custom without theme.css, skip the import and style everything with your own classes.

Patterns

Hook vs Component for Plugins

Hooks — always active, configure at mount:

tsx
useHls(player);
useVast(player, { tagUrl: "..." });

Components — conditional activation via JSX:

tsx
{showAds && <Vide.VastPlugin tagUrl="..." />}

Use hooks when the plugin is always needed. Use components when you need conditional rendering.

Direct Player Access

player.current gives you direct access to the player instance:

tsx
const player = useVidePlayer();

player.current?.play();
player.current?.pause();
player.current?.currentTime;

return (
  <>
    <Vide.Root player={player}>
      <Vide.UI>
        <Vide.Video src="video.mp4" />
      </Vide.UI>
    </Vide.Root>
    <button onClick={() => player.current?.pause()}>Pause</button>
  </>
);

Ad Plugins (OMID, SIMID, VPAID)

omid(), simid(), and vpaid() are ad-level plugins, not player-level. Pass them via the adPlugins option:

tsx
import { omid } from "@videts/vide/omid";
import { simid } from "@videts/vide/simid";
import { vpaid } from "@videts/vide/vpaid";

useVast(player, {
  tagUrl: "https://...",
  adPlugins: (ad) => [
    omid({ partner: { name: "myapp", version: "1.0" } }),
    vpaid({ container: adContainerRef.current! }),
    simid({ container: adContainerRef.current! }),
  ],
});

Ad Container

VPAID and SIMID render interactive content inside a container element that must overlay the player. When using the UI plugin, the container needs z-index: 3 to sit above the UI's click overlay:

tsx
function Player() {
  const player = useVidePlayer();
  const adContainerRef = useRef<HTMLDivElement>(null);

  useVast(player, {
    tagUrl: "https://...",
    adPlugins: () => [
      vpaid({ container: adContainerRef.current! }),
    ],
  });

  return (
    <Vide.Root player={player}>
      <Vide.UI>
        <Vide.Video src="video.mp4" />
        <Vide.Controls>...</Vide.Controls>
      </Vide.UI>
      <div
        ref={adContainerRef}
        style={{
          position: "absolute",
          top: 0, left: 0, width: "100%", height: "100%",
          zIndex: 3,
          pointerEvents: "none",
        }}
      />
    </Vide.Root>
  );
}

The ad container children need pointer-events: auto — the VPAID/SIMID plugins set this on their slot elements automatically.

Custom Components

Build your own player components using useVideContext() and useVideEvent(). All built-in components follow this same pattern.

Basics

useVideContext() returns Player | null from context. Must be inside <Vide.Root>.

tsx
import { useVideContext, useVideEvent } from "@videts/vide/react";

function CurrentTime() {
  const player = useVideContext();
  const [time, setTime] = useState(0);

  useVideEvent(player, "timeupdate", ({ currentTime }) => {
    setTime(currentTime);
  });

  return <span>{Math.floor(time)}s</span>;
}

Use it inside <Vide.Controls> (or anywhere within <Vide.Root>):

tsx
<Vide.Controls>
  <Vide.PlayButton />
  <CurrentTime />
</Vide.Controls>

Subscribing to State Changes

tsx
function StateIndicator() {
  const player = useVideContext();
  const [state, setState] = useState("idle");

  useVideEvent(player, "statechange", ({ to }) => {
    setState(to);
  });

  return <div className="my-state-badge">{state}</div>;
}

Calling Player Methods

Access player directly for actions. Guard with if (!player) since it's null before mount.

tsx
function SkipButton({ seconds = 10 }: { seconds?: number }) {
  const player = useVideContext();

  const onClick = useCallback(() => {
    if (!player) return;
    player.currentTime = Math.min(
      player.currentTime + seconds,
      player.el.duration,
    );
  }, [player, seconds]);

  return <button onClick={onClick}>+{seconds}s</button>;
}

Ad-Aware Components

Use useAdState() for components that react to ad playback.

tsx
import { useVideContext, useAdState } from "@videts/vide/react";

function ContentOverlay() {
  const player = useVideContext();
  const { active } = useAdState(player);

  if (active) return null; // hide during ads
  return <div className="my-overlay">...</div>;
}

Available Hooks

HookPurpose
useVideContext()Get Player | null from context
useVideEvent(player, event, handler)Subscribe to player events with auto-cleanup
useAdState(player)Get { active, meta } for ad state
useAutohide(containerRef, player)Auto-hide controls on inactivity
useKeyboard(containerRef, player)Keyboard shortcuts (space, arrows, etc.)

Import Styles

tsx
// Namespace style
import { Vide, useVidePlayer, useHls } from "@videts/vide/react";
<Vide.Root player={player}>
  <Vide.UI>
    <Vide.Video />
  </Vide.UI>
</Vide.Root>

// Individual imports
import { VideRoot, VideUI, VideVideo, VideControls, PlayButton } from "@videts/vide/react";
<VideRoot player={player}>
  <VideUI>
    <VideVideo />
  </VideUI>
</VideRoot>

Full Example

tsx
import { useVidePlayer, useHls, useVideEvent, Vide } from "@videts/vide/react";
import "@videts/vide/ui/theme.css";

function VideoPlayer({ src, adTag }: { src: string; adTag?: string }) {
  const player = useVidePlayer();
  useHls(player);

  useVideEvent(player, "statechange", ({ from, to }) => {
    console.log(`${from} → ${to}`);
  });

  return (
    <Vide.Root player={player}>
      {adTag && <Vide.VastPlugin tagUrl={adTag} />}
      <Vide.UI>
        <Vide.Video src={src} />
        <Vide.AdLabel />
        <Vide.AdCountdown />
        <Vide.AdSkip />
        <Vide.AdLearnMore />
        <Vide.Controls>
          <Vide.PlayButton />
          <Vide.Progress />
          <Vide.TimeDisplay />
          <Vide.Volume />
          <Vide.FullscreenButton />
        </Vide.Controls>
      </Vide.UI>
    </Vide.Root>
  );
}