Version 4 is in alpha. It can still change between releases, and it isn't on npm yet. Version 3 docs
Migrating from version 3
What changed in version 4, and how to move a player over.
Version 4 is a rewrite, because almost everything in version 3 lived in one context that re-rendered every part as time changed. The ideas you know, such as Video.Root and asChild, carry over. Most of the work is listing plugins and renaming some parts.
At a glance
| Area | Version 3 | Version 4 |
|---|---|---|
| Import | import { Video } from "react-video-kit" | import * as Video from "react-video-kit" |
| CSS | Included automatically | import "react-video-kit/styles.css" |
| Modules | ESM and CommonJS | ESM only |
| Dependencies | motion as a peer, plus Radix and others | None, except the optional peers hls.js and dashjs |
| Features | Props on Root | Plugins |
| Quality levels | src array | src={{ renditions: [...] }} |
A src array means | Quality levels | Fallbacks, tried in order |
autoPlay | Default on | Default off |
| Video fit | cover | contain |
| Ref | Methods on the handle | The Player |
A full example
Here's a typical player in version 3:
import { Video } from "react-video-kit";<Video.Root src="/video.mp4" title="The long ridge" tracks={[{ src: "/en.vtt", kind: "captions", srclang: "en", label: "English" }]} hotkeys autoPlay={false}> <Video.Media /> <Video.Backdrop /> <Video.Center> <Video.PlayPause /> <Video.Loading /> </Video.Center> <Video.Footer> <Video.Title /> <Video.Timeline /> <Video.Time.Current /> <Video.CaptionsMenu /> <Video.FullscreenToggle /> </Video.Footer></Video.Root>;If you don't need that exact layout, this is all you need in version 4:
"use client";import { captions } from "react-video-kit/captions";import { fullscreen } from "react-video-kit/fullscreen";import { hotkeys } from "react-video-kit/hotkeys";import { idle } from "react-video-kit/idle";import { VideoPlayer } from "react-video-kit/player";import "react-video-kit/styles.css";const plugins = [ idle(), hotkeys(), captions({ tracks: [{ src: "/en.vtt", language: "en", label: "English" }] }), fullscreen(),];export function Player() { return <VideoPlayer src="/video.mp4" plugins={plugins} />;}To keep a custom layout, use the styled parts. The tables below show what each part became.
Update your imports and add the CSS
npm install react-video-kit@alphanpm uninstall motionimport * as Video from "react-video-kit";import "react-video-kit/styles.css";The package is ESM only. CommonJS apps need an ESM-capable bundler. If you use Tailwind, declare the layer order.
Replace Root props with plugins
Create plugins in a client file, outside your component.
| Version 3 prop | Version 4 |
|---|---|
tracks, defaultTrackIndex | captions({ tracks }) |
hotkeys | hotkeys() |
autoPlay | autoplay(), which also handles browsers refusing to start |
showControls | Leave out Video.Controls |
title, subtitle | Your own element, as a child of the player |
poster | On Video.Media or VideoPlayer |
loop | On Video.Media, or media={{ loop: true }} on VideoPlayer |
defaultQualityIndex | src={{ initial, renditions }} |
onProgress | useClock, or onTimeUpdate on Video.Media |
onDuration, onEnded, onPlay, onPause, onLoad, onVolumeChange | The matching event on Video.Media, such as onEnded or onDurationChange |
onTrackChange | useVideoState with the captions plugin, reading active |
onQualityChange | useVideoState on media.quality |
Controls that used to be always available need a plugin now:
| Feature | Plugin |
|---|---|
| Fullscreen | fullscreen() |
| Picture in picture | pip() |
| Controls that hide when idle | idle() |
| Captions menu | captions() |
| Quality menu | quality() |
Change quality lists
A src array used to mean quality levels. Now it means fallbacks, so a list of qualities would silently only ever play the first.
// Version 3<Video.Root src={[ { src: "/1080.mp4", type: "video/mp4", label: "HD" }, { src: "/720.mp4", type: "video/mp4", label: "SD" },]} />// Version 4<Video.Root plugins={[quality()]} src={{ renditions: [ { src: "/1080.mp4", type: "video/mp4", label: "HD" }, { src: "/720.mp4", type: "video/mp4", label: "SD" }, ], }}/>Rename parts
| Version 3 | Version 4 |
|---|---|
Video.Root, Video.Media, Video.PlayPause | Same names |
Video.Timeline | Same name. trackClassName, rangeClassName, and thumbClassName become subparts |
Video.Volume.Button | Video.Mute |
Video.Volume.Slider | Video.VolumeSlider |
Video.Time.Current | Video.CurrentTime |
Video.Time.Remaining | Video.RemainingTime |
Video.Loading | Video.BufferingIndicator |
Video.FullscreenToggle | Fullscreen from react-video-kit/fullscreen |
Video.PipToggle | Pip from react-video-kit/pip |
Video.CaptionsMenu | CaptionsMenu from react-video-kit/captions |
Video.QualityMenu | QualityMenu from react-video-kit/quality |
Video.SeekBack, Video.SeekForward | Your own button |
Video.Header, Video.Center, Video.Footer, Video.Backdrop | Video.Controls, Video.ControlRow, Video.ControlGroup, or your own elements |
Video.Title, Video.Subtitle | Your own elements |
Named exports such as VideoPlayPause and VideoRoot are gone, because import * as Video removes unused parts anyway.
Update refs
ref gives you the Player now.
| Version 3 handle | Version 4 |
|---|---|
play(), pause(), seek(time) | commands.play(), commands.pause(), commands.seek(time) |
setVolume(value) | commands.setVolume(value) |
mute(), unmute() | commands.setMuted(true), commands.setMuted(false) |
enterFullscreen(), exitFullscreen() | plugin(fullscreen).commands.toggle() |
enterPip(), exitPip() | plugin(pip).commands.toggle() |
Custom controls
useVideoContext still exists, in react-video-kit/compat, with the same shape. Change the import and your control keeps working:
import { useVideoContext } from "react-video-kit/compat";It re-renders four times a second, so it's only a bridge, and it goes away in version 5. Move each control to the hooks when you can:
// Beforeconst { isPlaying, handlePlay, handlePause } = useVideoContext();// Afterconst paused = useVideoState((media) => media.paused);const { play, pause } = useCommands();useVideoContext | Version 4 |
|---|---|
isPlaying, isMuted, volume, duration, playbackRate | useVideoState on media.paused, muted, volume, duration, rate |
isLoading | useVideoState on media.status === "loading" || media.waiting |
currentTime, remainingTime | useCurrentTime, or useClock |
isFullscreen, isPip | useVideoState(fullscreen, (state) => state.active), and the same for pip |
handleSeek, backBy, forwardBy | useCommands().seek |
handleVolumeChange, handlePlaybackRateChange | useCommands().setVolume, setRate |
videoRef, wrapperRef | usePlayer().video(), root() |
Styling
Classes with the rv- prefix, such as rv-flex, were internal Tailwind utilities, and they're gone. Use your own CSS or Tailwind classes, and read Theming for how to change the skin.
Parts no longer take *ClassName props. Use data-part or subparts.
Behaviour you'll notice
These changes are improvements, but you may have depended on the old behaviour.
- Sources load when they change. Changing
srcused to do nothing. It now loads the new source. - Failed sources show an error. They used to spin forever. There's now an error message with a stable code, and a retry button when trying again could help.
onErroron the player hears about every failure. - Controls stay visible while paused, with the idle plugin.
crossOriginis opt-in. Version 3 always set it. Pass it throughmediaorVideo.Mediawhen you need it, such as for captions on another origin.- Menus stay inside the player. They used to portal to
document.body, and now they render inside the root, so they work in fullscreen with no special handling.