Duck Media Slider
The waterline along the edge of a film: a dimmer fill for what has loaded, a readout that runs ahead of the thumb, and a 4px dense line that only grows a duck when you reach for it.
InputsClient component
Preview
Installation
The CLI writes the source into your project and pulls in whatever it depends on.
$
pnpm dlx shadcn@latest add @duck/duck-media-sliderPulls in @duck/theme. Already installed items are skipped.
Usage
import { DuckMediaSlider, formatTimecode } from "@/components/ui/duck-media-slider"The file lands in components/ui/duck-media-slider.tsx and belongs to you from that point on. Edit it in place rather than wrapping it.
Props
Everything not listed here is forwarded to the underlying element.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue | number | 0 | Playhead, in the same unit as min and max — seconds, normally. Ignored while a drag is in flight. |
| buffered | number | 0 | How much has loaded, as a fraction of the whole track from 0 to 1 — not a value in min..max, so buffered.end(buffered.length - 1) / duration drops straight in. |
| preview | (value: number) => ReactNode | - | Readout that follows the pointer, and the keyboard, before commit. It returns a node, so a thumbnail fits where a timecode would go. |
| dense | boolean | false | 4px track with the thumb on hover and focus only, for the bottom edge of a video. The default stays settings-panel sized. |
| onScrub | (value: number) => void | - | Every step of a drag or key repeat, while the drag is still in flight. For a timecode readout, not for seeking. |
| onSeek | (value: number) => void | - | Once, on pointer-up, key-up or blur. This is the one that moves the playhead. |
| formatValue | (value: number) => string | formatTimecode | Feeds aria-valuetext. Defaults to m:ss, because a bare number of seconds tells a listener nothing. |
| min / max / step / disabled | native | - | It is an <input type="range">, so arrows, Home/End, PageUp/PageDown, touch dragging and RTL are the browser's job. |
Rules
What keeps this component from turning into noise.
- While a drag is in flight the component owns the value and ignores the value prop. That is the point: a <video> fires timeupdate four times a second and would drag the thumb back under your finger.
- Wire onSeek to video.currentTime and onScrub to your readout. Seeking on every scrub step stalls the element.
- buffered is a 0–1 fraction of the track, never a time. Media with holes in it: pass the range holding the playhead.
- Dense is for an overlay on a picture. In a settings panel use the default size, where the thumb is always visible.
