Transport Controls
transport-controlsControlsPlay, stop, loop, a tempo you can drag, and a division that means something.
$ pnpm dlx shadcn@latest add @pluginui/core-transport-controlsInstall
Copies the source into your project. Anything it depends on comes with it. Run this in your React project with shadcn initialized; no registry configuration is needed.
Pulls in
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-transport-controls.jsonUsage
Connect the documented props to your application or host.
import { TransportControls } from "@/components/ui/transport-controls"
// Fully controlled. The host owns the clock and passes the position in.
<TransportControls
playing={playing}
onPlayingChange={setPlaying}
onStop={() => { setPlaying(false); rewind() }}
loop={loop}
onLoopChange={setLoop}
tempo={tempo}
onTempoChange={setTempo}
division={division}
onDivisionChange={setDivision}
beat={beatsElapsed}
/>
// Record only appears when it can do something.
<TransportControls recording={armed} onRecordChange={setArmed} /* ... */ />
// No callbacks: a status strip beside a rendered result.
<TransportControls playing loop tempo={174} division="1/16" />About Transport Controls
A bar across the top of every sequencer-shaped interface, composed from existing controls rather than new button code. Its four keys use glow-button at a small size and its existing modes: toggle for play, loop and record; momentary for stop. This preserves the shipped halo, LED bezel, press-in seat and keyboard behavior. The monospace tempo readout responds to vertical drag, wheel, arrow keys or double-click reset. Shift gives a four-times-slower tenth-bpm adjustment anchored where Shift was pressed. The TAP key stores only timestamps. A pure median-averaged reducer in lib/musical-time calculates bpm and sends it immediately through onTempoChange, so a fumbled tap has no effect. The division row shows the value you need beside the selection: at the current tempo, choosing 1/8T displays 208 ms and 4.80 Hz. The beat indicator pulses only from a phase or beat prop, because a display timer quickly disagrees with an audio clock. The host owns the clock, as it does for lfo-visualizer's phase and step-grid's playhead. Each control is read-only when its callback is absent; without callbacks, the component is a status strip.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| playing / onPlayingChange | boolean / (playing) => void | — | Whether the transport is rolling. The control is controlled: the key reports the state and never latches locally. Without the callback, it becomes a lamp showing that state. |
| onStop | (event) => void | — | Momentary, and purely a report: whether stopping also clears playing, rewinds or disarms the record is the host's decision and the host's state. Absent hides the key. |
| loop / onLoopChange | boolean / (loop) => void | — | Loop state, same contract as play. |
| recording / onRecordChange | boolean / (recording) => void | — | Record arm. The key is hidden when the callback is absent, because a transport that cannot record should not show a control that suggests it can. |
| tempo / onTempoChange | number / (bpm) => void | — | Tempo in bpm, clamped 20..300 (the band the session transport already uses). Drag, wheel, arrows, double-click-to-default and tap all report through the callback; without it the readout is a readout. |
| tempoStep / tempoFineStep | number | 1 / 0.1 | What a drag, a wheel notch or an arrow key lands on, plain and with Shift held. Snapping goes through knob-core's snapToStep, so a tempo lands on its step exactly as a knob's value does. |
| defaultTempo | number | 120 | Where a double-click on the readout sends the tempo. |
| division / divisions / onDivisionChange | NoteDivisionId / NoteDivisionId[] / (id) => void | "1/8" / 1/4, 1/8, 1/8T, 1/16 | The picker uses IDs from lib/musical-time's table: eighteen values from a whole note to a 1/32, each in plain, dotted and triplet flavours. Labels include 1/8, 1/8• and 1/8T. |
| showDivisionReadout | boolean | true | Print the current division's length and rate beside the row. This is what makes a division mean something: the same interval in milliseconds and in steps per second, the two units a host actually wires to. |
| phase / beat | number | null | — | The host's clock, and the only thing that lights the beat indicator. phase is 0..1 inside the current beat; beat is an absolute fractional beat index and additionally tells the indicator which hits are downbeats. Nothing in the component animates a counter. |
| beatsPerBar | number | 4 | Beats per bar for the downbeat accent. Beats are quarter notes; there is no time-signature model behind this value. |
| showTap / showTempo / showBeat | boolean | — | Drop the parts a given transport does not want. The tap key defaults to visible whenever onTempoChange is present, and to hidden when it is not. |
| size | number | 52 | Key diameter in pixels. The tempo digits and the tap key scale off it. |
| recordColor | string | var(--uikit-record, #ef4444) | The record key's colour is red, as it has been since 1955, and the theme has no token for it. This uses an opt-in variable with a red fallback, allowing a skin to claim --uikit-record without adding a token to control-theme. |
| onInteraction | (event) => void | — | Fires once, synchronously inside the first gesture of any kind. Use it to resume a suspended AudioContext; this is why a transport is usually the first control on the page. |
| color | string | — | Override the accent colour for this instance. |
Interaction
- Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
- Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
- Double-click resets to
defaultValue. - Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
- Proper
sliderandswitchroles with livearia-valuetext, so a screen reader announces the formatted value rather than the raw number. - The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.