Scrubber
A keyboard-accessible seek bar over the whole composition.
<Scrubber.*> is a seek bar: press or drag anywhere on it to seek. The played part and the knob move on the compositor, so playback doesn't re-render it every frame.
Example
import { formatClock } from "@miraiclip/editor";
import { Scrubber, useDurationUs, useProjectState } from "@miraiclip/react";
// Inside <ProjectProvider> and <PlaybackProvider>.
export function SeekBar() {
// Whole seconds: this label re-renders once a second while playing, not every frame.
const second = useProjectState((s) => Math.floor(s.playheadUs / 1_000_000));
const durationUs = useDurationUs();
return (
<div className="flex items-center gap-3 text-xs tabular-nums text-neutral-400">
<span className="w-10">{formatClock(second * 1_000_000)}</span>
<Scrubber.Root className="h-5 flex-1 cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-sky-400/60">
{/* The track, then the played part on top of it (Range scales from its left edge). */}
<div className="absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 overflow-hidden rounded-full bg-neutral-700">
<Scrubber.Range className="h-full w-full bg-sky-400" />
</div>
{/* Thumb is a full-size layer slid by the played fraction: the knob sits at its left edge. */}
<Scrubber.Thumb>
<div className="absolute top-1/2 left-0 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-sky-400 bg-white" />
</Scrubber.Thumb>
</Scrubber.Root>
<span className="w-10 text-right">{formatClock(durationUs)}</span>
</div>
);
}Anatomy
<Scrubber.Root> // the slider: pointer and keyboard seeking
<Scrubber.Range /> // the played part, scaled from its left edge
<Scrubber.Thumb> // a full-size layer slid by the played fraction
<div /> // your knob, at left: 0
</Scrubber.Thumb>
</Scrubber.Root>Parts
Root
An ARIA slider (role="slider", focusable) over the composition's length. It sets aria-valuetext to the time, for example "0:04 of 0:12".
| Prop | Default | Description |
|---|---|---|
stepUs | 1 s | Arrow-key step |
largeStepUs | 5 s | Shift + arrow, Page Up and Page Down step |
| Key | Seeks |
|---|---|
| → / ↑, ← / ↓ | One step forward or back (Shift: a large step) |
| Page Up, Page Down | One large step |
| Home, End | To the start or the end |
It re-renders about once a second while playing, for the screen-reader value. It is position: relative; give it a height.
Range
Scaled horizontally by the played fraction, from its left edge (transform-origin: left). Size it to the full track; to round its ends, clip it with a wrapper that has overflow: hidden and the track's radius, as in the example.
Thumb
A layer at position: absolute; inset: 0, slid right by the played fraction. Put the knob inside it at left: 0 and center it with translate(-50%, …). It ignores pointer events, so presses reach the root.
Notes
- The bar covers the composition from 0 to the end of the last clip. For a seek bar over a zoomed part of the timeline, use the timeline's
<Timeline.Ruler>. - Pass
aria-labelto the root to replace the default "Seek". - Show the time next to the bar with a coarse selector, as in the example:
useProjectState((s) => Math.floor(s.playheadUs / 1_000_000))re-renders once a second.usePlayheadUs()would re-render every frame.