Scrubber is the media seek control. It owns the track, the handle, the elapsed and total timestamps, the focused-value tooltip, the interaction scrim, and all keyboard and pointer seeking. You supply a controlled position and two callbacks: one for every intermediate update and one for the committed value.0–100 scale — a percentage of the media, not seconds. Pass durationSeconds separately so the component can format times.import { useState } from 'react';
import { Scrubber, ScrubberTimestampPosition } from '@wearables-ui-toolkit/mrbd';
type PlaybackScrubberProps = {
positionPercent: number;
durationSeconds: number;
onSeek: (percent: number) => void;
};
export function PlaybackScrubber({
positionPercent,
durationSeconds,
onSeek,
}: PlaybackScrubberProps) {
const [preview, setPreview] = useState<number | null>(null);
return (
<Scrubber
value={preview ?? positionPercent}
durationSeconds={durationSeconds}
timestampPosition={ScrubberTimestampPosition.BOTTOM}
showTooltip
onValueChange={setPreview}
onValueChanged={next => {
setPreview(null);
onSeek(next);
}}
aria-label="Playback position"
/>
);
}
.player-controls {
display: flex;
flex-direction: column;
gap: var(--uit-spacing-small);
padding-inline: var(--uit-spacing-large);
}
ScrubberProps.| Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Current position on a 0–100 scale. Clamped into range; a non-finite value is treated as 0. |
durationSeconds | number | 0 | Total media duration in seconds. Floored to a whole, non-negative number; a non-finite value becomes 0. 0 hides the timestamp row. |
showTooltip | boolean | false | Show a tooltip anchored to the handle while the control is focused or being dragged. |
hideScrim | boolean | false | Skip rendering the interaction scrim behind the control. |
timestampPosition | ScrubberTimestampPosition | ScrubberTimestampPosition.BOTTOM | Place the elapsed and total timestamps above or below the track. |
onValueChange | (value: number) => void | — | Live updates: every pointer move during a drag, and every keyboard step that changes the value. |
onValueChanged | (value: number) => void | — | Committed value: once per keyboard step that changes the value, and once at the end of a pointer drag. |
disabled | boolean | false | Disables seeking, removes the tab stop, dims the control, and marks the element as disabled. |
tabIndex | number | 0 | Tab order of the control. Forced to -1 while disabled. |
aria-label | string | 'Media position' | Accessible name. Override with something specific to the media. |
ScrubberProps also accepts the standard div attribute surface — className, style, dir, data-*, and DOM event handlers. role, aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext are removed from the accepted props: the component always supplies them. onChange is removed too — use onValueChange and onValueChanged.onFocus, onBlur, onKeyDown, onKeyUp, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, and onLostPointerCapture handlers are preserved and run first, before the component’s own handling.ScrubberTimestampPositionBOTTOM — timestamps sit below the track.TOP — timestamps sit above the track.value. Apply the callback value to your state and pass it back in.onValueChange and then onValueChanged. Up and Down are not consumed, so vertical focus navigation still works.value prop it rendered, so quick repeated presses keep moving even before your state update lands. The component re-syncs to value whenever the prop changes and no drag is active.onValueChange continuously. Releasing, cancelling, or losing pointer capture emits onValueChanged exactly once with the last value. Secondary buttons are ignored, and a second pointer cannot start a competing drag.preventDefault() inside your onKeyDown or onPointerDown stops the component from acting on that event.durationSeconds is greater than zero. The elapsed label is derived from value and the duration; both are formatted as minutes and seconds, extended with hours for long media. The timestamp row is hidden from assistive technology because the same information is in the control’s value text.showTooltip, a tooltip tracks the handle while the control is focused or being dragged and is hidden while disabled. It shows the elapsed time when a duration is set and the rounded percentage otherwise. The tooltip renders through the floating portal root, so it escapes any clipped ancestor; see Floating portals.hideScrim is set, a scrim is drawn behind the control and fades in during interaction so the track and timestamps stay legible over bright, busy media on the additive-light display.disabled blocks keyboard stepping and pointer seeking alike, removes the tab stop, keeps the tooltip hidden, switches the cursor to not-allowed, and dims the control with the disabled opacity token. The control stays in the accessibility tree, marked aria-disabled.role="slider" with aria-orientation="horizontal", aria-valuemin of 0, aria-valuemax of 100, and aria-valuenow set to the clamped position.aria-valuetext announces elapsed and total time when a duration is set, and the rounded percentage otherwise.aria-label is generic. Provide one that names the media or the player.disabled, aria-disabled is set and the tab stop is removed, so the control stays announced but is skipped by sequential focus.onValueChange for cheap live feedback such as a preview position, and onValueChanged for the expensive commit: the seek request, analytics, or persistence.durationSeconds whenever you know it, so users get real times instead of a percentage.value once the seek is applied.value; convert to a 0–100 position first.onValueChange as the commit — it fires for every pointer move.Scrubber over SliderBar when time semantics matter.