React Playback Scrubber

A post-mortem is much easier when you can step through the recording. Play, pause, scrub, step one frame at a time, and jump straight to the moment the interlock tripped.

Chamber pressure

769.14 Torr

00:16.00frame 480 / 89900:29.96

Installation

npx shadcn@latest add "https://kelvinui.com/registry/playback-scrubber.json"

Props

PropTypeDescription
frameCountnumberTotal frames in the recording
fpsnumberNominal frames per second at 1× (default 30)
markersTimelineMarker[]{ frame, label, kind? }
speedsnumber[]Speed options (default 0.25–4)
formatFrame(frame: number) => stringTimestamp formatter
onChange(frame: number) => voidFires on every frame change
children(frame: number) => ReactNodeRender your own view of the current frame

Time-based advance, not frame-per-tick

Playback advances by elapsed wall time, not one frame per animation callback. Accumulating dt × fps × speed and stepping by the whole part means a recording plays at the same real-world rate on a 60 Hz laptop and a 144 Hz monitor, and 0.5× is genuinely half speed rather than "however fast this display happens to run". Naively advancing one frame per requestAnimationFrame is the common bug and it makes every measurement taken off the replay wrong.

The component owns the transport and hands you the frame number through a render prop, so it does not care what you are replaying — a chart, a wafer map, a P&ID mimic, a video frame. The demo replays a chamber-pressure trace with a rolling window.

Markers are clickable in the legend as well as visible on the timeline, colour-coded by severity, so "jump to the interlock trip" is one click rather than a hunt. Scrubbing pauses playback, which is what people expect and what stops the playhead fighting the pointer.

New components every week

Get the week's new Kelvin UI components and templates in one short email. No spam, unsubscribe anytime.