React
React hooks and components for Vide. Import from @videts/vide/react.
npm install @videts/vide react react-domQuick Start
No UI
Core only — no visual controls. Build everything yourself or use your own components.
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.
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.
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.
const player = useVidePlayer();player.current—Player | null.nulluntil the video element mounts.- Pass
playerto<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.
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).
useUivs<Vide.UI>—useUiis a hook that mounts the vanilla JSui()plugin (which auto-generates DOM) onto React's lifecycle. When building UI with React components like<Vide.UI>+<Vide.PlayButton>,useUiis 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.
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.
<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 byuseVidePlayer().
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.
<Vide.UI>
<Vide.Video src="video.mp4" />
<Vide.Controls>...</Vide.Controls>
</Vide.UI>- All standard
<div>HTML attributes are passed through. classNameis appended aftervide-ui.
Vide.Video
Renders a <video> element and binds the player to it. Must be inside <Vide.Root>.
<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.
<Vide.Controls>
<Vide.PlayButton />
<Vide.Progress />
<Vide.TimeDisplay />
<Vide.Volume />
<Vide.FullscreenButton />
</Vide.Controls>- All standard
<div>HTML attributes are passed through. classNameis appended aftervide-controls.
Plugin Components
Render nothing (null). Use for conditional plugin activation. Place as children of <Vide.Root>.
<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.
<Vide.Controls>
<Vide.PlayButton />
<Vide.Progress />
<Vide.Volume />
<Vide.TimeDisplay />
<Vide.FullscreenButton />
<Vide.MuteButton />
</Vide.Controls>| Component | Default Class | Props | State Attributes |
|---|---|---|---|
PlayButton | vide-play | className, children | data-playing |
MuteButton | vide-mute | className, children | data-muted |
Progress | vide-progress | className | data-disabled, --vide-progress, --vide-progress-buffered |
Volume | vide-volume | className, children | data-muted, --vide-volume |
FullscreenButton | vide-fullscreen | className, children, target | data-fullscreen |
TimeDisplay | vide-time | className, 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.classNameis 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.
<Vide.AdLabel />
<Vide.AdCountdown />
<Vide.AdSkip />
<Vide.AdLearnMore />| Component | Default Class | Props |
|---|---|---|
AdLabel | vide-ad-label | className, children |
AdCountdown | vide-ad-countdown | className, format |
AdSkip | vide-skip | className, children |
AdLearnMore | vide-ad-cta | className, children |
AdOverlay | vide-ad-overlay | className, children |
useAdState(player)
Low-level hook for custom ad UI. Returns { active, meta }.
const player = useVideContext();
const { active, meta } = useAdState(player);
if (active && meta?.clickThrough) {
// render custom CTA
}active—boolean, true during ad playback.meta—AdMeta | nullwithadId,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.
<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:
useHls(player);
useVast(player, { tagUrl: "..." });Components — conditional activation via JSX:
{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:
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:
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:
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>.
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>):
<Vide.Controls>
<Vide.PlayButton />
<CurrentTime />
</Vide.Controls>Subscribing to State Changes
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.
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.
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
| Hook | Purpose |
|---|---|
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
// 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
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>
);
}