Duck Player Bar
The docked transport: a play button that stays put, an elapsed read-out that follows the drag instead of the playhead, and a seek bar that speaks in words.
BlockClient componentlucide-react
Preview
0:413:34
DSto costruendo il mio design systemdacoder · episode 12
Installation
One command writes the section and every component it renders.
$
pnpm dlx shadcn@latest add @duck/duck-player-barThe block lands in components/blocks/duck-player-bar.tsx. It builds on duck-media-slider, duck-volume and quack-button, which the CLI installs alongside it. Already installed items are skipped.
Usage
import { DuckPlayerBar, clock } from "@/components/blocks/duck-player-bar"A block is a starting point, not a widget. The props exist so the example renders with real content — once the file is in your project, hard-code what never changes and delete the rest.
Props
Everything not listed here is forwarded to the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
| art | React.ReactNode | - | Artwork or mark beside the title. |
| title | React.ReactNode | - | What is playing. |
| subtitle | React.ReactNode | - | Artist, series, episode. |
| duration | number | - | Seconds. Leave it undefined for a live stream — the bar disables the seek and prints live rather than 0:00. |
| position | number | 0 | Playhead in seconds. |
| buffered | number | - | Fraction of the whole track that has loaded, 0 to 1. |
| playing | boolean | false | Drives the icon and the button's accessible name. |
| onPlayingChange | (playing: boolean) => void | - | Fires from the button and from Space when shortcuts are on. |
| onSeek | (seconds: number) => void | - | Once, on release. This is the one that seeks. |
| onScrub | (seconds: number) => void | - | Every step of a drag, while it is still in flight. |
| onPrevious | () => void | - | Renders the previous button when passed. |
| onNext | () => void | - | Renders the next button when passed. |
| volume | number | - | 0 to 1, straight from video.volume. |
| muted | boolean | - | Independent of volume, as in the media element. |
| preview | (seconds: number) => React.ReactNode | - | Thumbnail or chapter name under the pointer, before commit. |
| actions | React.ReactNode | - | Right of the volume: repeat, shuffle, queue, cast. |
| shortcuts | boolean | false | Bind Space and the arrows globally. Off by default, and even on it ignores inputs, textareas and contenteditable. |
| seekStep | number | 10 | Seconds an arrow key jumps when shortcuts are on. |
Rules
What keeps this section from turning into noise.
- The transport sits in a fixed middle grid column. With justify-between the play button slides as the title changes length, and the target a reader aims at should not move between tracks.
- The read-out follows the drag, not the playhead. DuckMediaSlider owns the value while a scrub is in flight, and digits that argue with the handle the reader is holding are worse than none.
- Digits for the eye, words for the ear: the label shows 1:04 and aria-valuetext says "1 minute 4 seconds of 3 minutes 20 seconds".
- Play is an action, not a toggle. One button whose icon and label swap, never aria-pressed — "Pause, pressed" tells a listener nothing they can act on.
- Shortcuts are opt-in and never fire from a text field. A player docked under a page must not eat Space from the search box above it.
