Headless
Behaviour and accessibility, with no CSS and no icons.
react-video-kit/primitives has the same parts as react-video-kit, but with no skin. You bring the design, and it brings the things that are hard to get right: a timeline that seeks without stutter, keyboard support, focus handling, and screen reader labels.
"use client";import * as Video from "react-video-kit/primitives";// No skin at all: the primitives bring behaviour and ARIA, and this file brings the look.export default function ComposeHeadless() { return ( <Video.Root src="/media/sample-720.mp4" load="visible" className="group relative aspect-video w-full overflow-hidden bg-black font-mono text-[#f6efe4]" > <Video.Media poster="/media/poster.jpg" className="block size-full object-cover" /> <div className="absolute inset-x-0 bottom-0 flex items-center gap-4 bg-gradient-to-t from-black/70 to-transparent px-5 pt-10 pb-4"> <Video.PlayPause className="group/play shrink-0 text-[13px] tracking-widest uppercase underline-offset-4 hover:underline"> <span className="group-data-[state=playing]/play:hidden">Play</span> <span className="hidden group-data-[state=playing]/play:inline">Pause</span> </Video.PlayPause> <Video.Timeline className="relative h-8 flex-1 cursor-pointer"> <Video.TimelineTrack className="absolute inset-x-0 top-1/2 h-px bg-white/40"> <Video.TimelineFill className="absolute inset-x-0 -top-px h-[3px] origin-left bg-[#ffb27a]" /> </Video.TimelineTrack> </Video.Timeline> <span className="shrink-0 text-[13px] tabular-nums"> <Video.CurrentTime /> / <Video.Duration /> </span> </div> </Video.Root> );}This player doesn't import any CSS from the package.
Style with state
Each part has a data-part attribute, and parts with state expose it as attributes. Use them from CSS or Tailwind.
<Video.PlayPause className="group"> <span className="group-data-[state=playing]:hidden">Play</span> <span className="hidden group-data-[state=playing]:inline">Pause</span></Video.PlayPause>The root has state attributes too, so a child can style itself from what the whole player is doing.
What's written to the DOM
TimelineFill and TimelineThumb move through transform, written directly by the clock, so don't set your own transform on them. This also applies to Tailwind 4's translate-* and scale-* utilities, which use properties that combine with it.
For the fill, set the origin and let it stretch across its track:
<Video.Timeline className="relative h-8 cursor-pointer"> <Video.TimelineTrack className="absolute inset-x-0 top-1/2 h-1 bg-white/30"> <Video.TimelineFill className="absolute inset-0 origin-left bg-white" /> </Video.TimelineTrack></Video.Timeline>Sliders are real inputs
Timeline and VolumeSlider each render a range input stretched invisibly over the part, which gives them focus, keyboard support, and screen reader announcements with no work from you. It's styled inline, so this works with no CSS.
To show a focus ring, style from :has(:focus-visible) on the part:
[data-part="timeline"]:has(:focus-visible) { outline: 2px solid currentColor;}Using plugins
Plugins work the same here, and their state, commands, and root attributes are all available. Their visual parts, such as Fullscreen, include icons and expect the skin. Their state and commands don't, so for a headless design, build your control from those:
"use client";import { useCommands, useVideoState } from "react-video-kit/primitives";import { fullscreen } from "react-video-kit/fullscreen";function FullscreenButton() { const active = useVideoState(fullscreen, (state) => state.active); const { toggle } = useCommands(fullscreen); return <button onClick={() => void toggle()}>{active ? "Exit" : "Enter"} fullscreen</button>;}All parts
See Primitives for each part and its props.