Knob
A rotary control, dragged up and down like the knobs of a DAW.
Anatomy
import { Knob } from '@addstack/daw-ui/react';
<Knob.Root>
<Knob.Label />
<Knob.Control>
<svg viewBox="0 0 100 100">
<Knob.Track />
<Knob.Range />
<Knob.Modulation />
<Knob.Pointer />
</svg>
</Knob.Control>
<Knob.Value />
</Knob.Root>Track, Range, Modulation and Pointer draw into a 100 × 100 view box around its center. You can leave them out and draw the knob yourself: the root sets --knob-angle, so rotate: var(--knob-angle) turns any element.
Examples
Bipolar
With origin, the range is drawn from that value instead of from min: a pan knob fills from the center in both directions, and its root gets data-bipolar.
Wrap
With wrap, turning past max comes around from min, as a phase or a hue does: max is the same point as min, so the value stays in [min, max), and the knob turns a full circle. The middle of the range points up, so -180 … 180 puts 0° at 12 o'clock.
Endless
An endless knob has no ends, like the encoders of hardware controllers: the value keeps counting past max and below min, and min … max is one turn. Use it to step through presets, move a selection or jog through time. details.delta gives each step, and a format can show what the count means:
<Knob.Root endless max={24} step={1} onValueChange={(_, { delta }) => browser.move(delta)} />It is a spinbutton without a minimum or maximum, since a slider needs both. Range draws nothing, and --knob-angle counts on past a turn, so a rotation never jumps back.
Modulation
A modulated knob shows how far modulation (an LFO, an envelope) moves it, lets the user change that depth, and shows where the modulation is now, as Serum and Vital do:
<Knob.Root value={level} onValueChange={setLevel} className="relative">
<Knob.Control>
<svg viewBox="0 0 100 100">
<Knob.Track />
<Knob.ModulationRange radius={48} />
<Knob.Modulation read={() => engine.modulated('level')} radius={48} />
<Knob.Range />
</svg>
</Knob.Control>
<Knob.ModulationDepth value={depth} onValueChange={setDepth} aria-label={t('lfoDepth')} className="absolute -top-2 -left-3" />
</Knob.Root>Knob.ModulationDepthis the depth: a handle beside the knob, asliderof its own from −100% to 100% of the knob's travel, so that a depth means the same on a linear, logarithmic or decibel knob. It drags, steps and resets as a knob does, in steps of 1%; a double-click or Delete takes the modulation away (0). Place it outsideKnob.Control, where you like: a corner, as in Serum.Knob.ModulationRangeis the arc from the value to the value plus the depth, or to either side of it withbipolar; it stops at the ends of the sweep, and follows the value and the depth without rendering. Without a handle, it showsdepth, for a depth set elsewhere, as in a modulation matrix.Knob.Modulationis where the modulation moves the value now:readreturns the modulated value in the knob's units, once per animation frame, and the arc is drawn without rendering.
A knob can have more than one modulation: give each range and its handle the same source.
<Knob.ModulationRange source="lfo" />
<Knob.ModulationRange source="envelope" bipolar />
…
<Knob.ModulationDepth source="lfo" aria-label={t('lfoDepth')} />
<Knob.ModulationDepth source="envelope" aria-label={t('envelopeDepth')} />Automation
read on the root plays automation the same way: called once per frame, shown without rendering. While the user holds the knob, their value wins. See gestures.
<Knob.Root read={() => automation.valueAt('cutoff', transport.time)} />Scales and steps
scale maps travel to value. A frequency knob with scales.log gives each octave the same travel; a MIDI knob with step={1} snaps to whole numbers and moves one step per arrow key.
<Knob.Root min={20} max={20_000} scale={scales.log} format={formats.frequency()} />
<Knob.Root min={0} max={127} step={1} />See values and formats for the scales and formats.
Interaction
| Input | Effect |
|---|---|
| Drag up / down | Increases / decreases. 200 px covers the full travel (sensitivity). |
| Shift while dragging or scrolling | 10 times finer. |
| Wheel | 5% of the travel per notch. wheel={false} leaves the wheel to the page. |
| ↑ → / ↓ ← | 1% of the travel (Shift: 0.1%), or one step. |
| Page Up / Page Down | 10% of the travel. |
| Home / End | Minimum / maximum. A wrapping knob's End goes to the last step before max; an endless knob has neither. |
| Double-click, Delete, Backspace | Reset to resetValue, defaultValue or origin. |
Each of these is one gesture.
API reference
Root
Groups the parts and holds the value. Renders a <div>.
Prop
Type
| Data attribute | |
|---|---|
data-dragging | While the value is being dragged. |
data-disabled | When disabled. |
data-bipolar | When origin lies inside the range. |
data-zone | The zone of zones the value is in: zones={{ high: 0.75 }} sets data-zone="high" above 0.75. |
Every part carries these attributes, so data-[zone=high]:stroke-red-500 works on Range directly.
| CSS variable | |
|---|---|
--knob-value | Travel of the value, from 0 to 1; on an endless knob, one per turn, without bounds. |
--knob-angle | Rotation of the value, e.g. -135deg at the minimum of a 270° sweep; on an endless knob, without bounds. |
Control
The focusable element that takes drags, keys and the wheel. Renders a <div> with role="slider" (role="spinbutton" when endless), aria-valuenow, aria-valuetext (the formatted value) and the label.
Prop
Type
Label
Names the knob for assistive technology; clicking it focuses the control. Renders a <span>. Without a label, give the control an aria-label.
Prop
Type
Track
The whole sweep. Renders an SVG <path>.
Prop
Type
Range
The arc from origin to the value. Renders an SVG <path>, without d on an endless knob.
Prop
Type
Pointer
A line at the value's angle. Renders an SVG <line>.
Prop
Type
Modulation
The arc from the value to the modulated value. Renders an SVG <path>.
Prop
Type
ModulationRange
The arc from the value over the depth of a modulation. Renders an SVG <path>.
Prop
Type
ModulationDepth
The handle for the depth of a modulation. Renders a <div> with role="slider".
Prop
Type
It carries data-dragging while dragged, and --knob-depth, its depth from −1 to 1, for styles that follow it.
Value
The formatted value. Renders an <output> with dir="auto", which is not a live region: the control already announces the value. The text changes without rendering; a children function renders this part on every change.
Prop
Type