Styled parts
Arrange the styled controls in your own layout.
Each part of the ready-made player is an export of react-video-kit. Import them all as a namespace, and tree-shaking still removes the ones you don't use.
"use client";import * as Video from "react-video-kit";import { fullscreen, Fullscreen } from "react-video-kit/fullscreen";import { idle } from "react-video-kit/idle";import { speed, SpeedMenu } from "react-video-kit/speed";const plugins = [idle(), speed(), fullscreen()];function SpeedTrigger() { const rate = Video.useVideoState((media) => media.rate); return <Video.MenuTrigger aria-label={`${rate}× speed`}>{rate}×</Video.MenuTrigger>;}export default function ComposeParts() { return ( <Video.Root src="/media/sample-720.mp4" plugins={plugins} load="visible"> <Video.Media poster="/media/poster.jpg" /> <Video.PlayOverlay /> <Video.Controls> <Video.ControlRow> <Video.ControlGroup> <Video.PlayPause /> <Video.Timeline /> <Video.Time /> </Video.ControlGroup> <Video.ControlGroup> <Video.Menu> <SpeedTrigger /> <Video.MenuContent> <SpeedMenu /> </Video.MenuContent> </Video.Menu> <Fullscreen /> </Video.ControlGroup> </Video.ControlRow> </Video.Controls> </Video.Root> );}Plugin controls, such as Fullscreen and SpeedMenu, come from the plugin's own import.
The basics
A player needs a Root and a Media. Everything else is optional, and it can go in any order inside Root:
"use client";import * as Video from "react-video-kit";import "react-video-kit/styles.css";export function Player() { return ( <Video.Root src="/video.mp4"> <Video.Media poster="/poster.jpg" /> <Video.PlayOverlay /> <Video.BufferingIndicator /> <Video.ErrorOverlay /> <Video.Controls> <Video.ControlRow> <Video.ControlGroup> <Video.PlayPause /> <Video.Timeline /> <Video.Time /> </Video.ControlGroup> <Video.ControlGroup> <Video.Volume /> </Video.ControlGroup> </Video.ControlRow> </Video.Controls> </Video.Root> );}Controls is what hides and shows with the idle plugin. ControlRow lays out its groups, and ControlGroup is one glass pill. A group that holds a Timeline stretches to fill the row and becomes a pill, as above.
VideoPlayer does it differently. Its Timeline sits directly in a row, which makes it a thick bare track with no pill behind it. Set data-position="scrubber" on that row to put the times underneath:
<Video.ControlRow data-position="scrubber"> <Video.Timeline /> <Video.CurrentTime /> <Video.RemainingTime /></Video.ControlRow>On players under 480px wide, the times move beside the track so it all fits on one line.
Video.Title is the video's name, with an optional line above it and a description below. Put it in the row above the timeline, as VideoPlayer does:
<Video.ControlRow data-position="details"> <Video.Title eyebrow="Season 1, Episode 1" title="The Green Coast" description="The swell meets the land." /> <Video.SettingsMenu /></Video.ControlRow>Transport
Transport is the row in the centre of VideoPlayer: a back button, play, and a forward button. seekSeconds sets how far they go, and defaults to 10.
<Video.Controls> <Video.Transport seekSeconds={15} /></Video.Controls>For one on its own, SeekButton takes seconds, and a negative number goes back. It's also in react-video-kit/primitives without styles.
<Video.SeekButton seconds={-30} />Mute
The speaker follows the volume. Each of its three waves fades on its own stretch of the level, and when it's muted, the speaker dims and a slash falls across it. Mute sets --rv-volume from 0 to 1, and to 0 while muted, so your own icon can do the same:
.my-icon { opacity: calc(0.4 + var(--rv-volume) * 0.6);}Timeline
Timeline works with no children. To change what's in it, list its subparts:
<Video.Timeline> <Video.TimelineTrack> <Video.TimelineBuffered /> <Video.TimelineFill /> <Video.Slot name="timeline-track" /> </Video.TimelineTrack> <Video.TimelineThumb /> <Video.TimelinePreview> <Video.Slot name="timeline-preview" /> </Video.TimelinePreview></Video.Timeline>Those two slots are where chapters and thumbnails draw, so keep them if you use those plugins.
Menus
SettingsMenu holds whatever plugins add to the menu slot, and hides itself when no row is left. A row hides by rendering nothing, so a copy of the rows stays mounted out of sight to notice that. If a row shows something that changes during playback, pass a component that reads it as value={<Current />}, because that only mounts in an open menu.
To build your own, use Menu, and put a plugin's menu in it:
import * as Video from "react-video-kit";import { SpeedMenu } from "react-video-kit/speed";<Video.Menu> <Video.MenuTrigger aria-label="Speed">1×</Video.MenuTrigger> <Video.MenuContent> <SpeedMenu /> </Video.MenuContent></Video.Menu>;MenuSub drills down in place, rather than opening a second menu beside the first. For your own options, use MenuRadioGroup:
<Video.MenuSub title="Ambience" value={ambience}> <Video.MenuRadioGroup value={ambience} onValueChange={setAmbience}> <Video.MenuRadioItem value="off">Off</Video.MenuRadioItem> <Video.MenuRadioItem value="on">On</Video.MenuRadioItem> </Video.MenuRadioGroup></Video.MenuSub>Menus render inside the player, so they keep working in fullscreen. They have keyboard support, and focus returns to the trigger when one closes.
Your own controls
Video.Button is a control that matches the others. Add behaviour with hooks:
"use client";import * as Video from "react-video-kit";function SkipBack({ seconds = 10 }: { seconds?: number }) { const player = Video.usePlayer(); const { seek } = Video.useCommands(); return ( <Video.Button aria-label={`Back ${seconds} seconds`} onClick={() => seek(player.clock.time() - seconds)}> ↺ </Video.Button> );}Use your own element
Controls built on Button, such as PlayPause, Mute, and Fullscreen, accept asChild. It merges their behaviour onto your element instead of rendering a button. This is useful for a design system's button component:
<Video.PlayPause asChild> <MyButton variant="ghost">Play</MyButton></Video.PlayPause>Your element must pass its props and ref through to the DOM.
All parts
See Components for every part and its props.