daw-ui

Knob

A rotary control, dragged up and down like the knobs of a DAW.

Cutoff
1.00 kHz

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.

Pan
C

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.

Phase
0°

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)} />
Preset
Init

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:

Level
Pan
<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.ModulationDepth is the depth: a handle beside the knob, a slider of 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 outside Knob.Control, where you like: a corner, as in Serum.
  • Knob.ModulationRange is the arc from the value to the value plus the depth, or to either side of it with bipolar; it stops at the ends of the sweep, and follows the value and the depth without rendering. Without a handle, it shows depth, for a depth set elsewhere, as in a modulation matrix.
  • Knob.Modulation is where the modulation moves the value now: read returns 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

InputEffect
Drag up / downIncreases / decreases. 200 px covers the full travel (sensitivity).
Shift while dragging or scrolling10 times finer.
Wheel5% 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 Down10% of the travel.
Home / EndMinimum / maximum. A wrapping knob's End goes to the last step before max; an endless knob has neither.
Double-click, Delete, BackspaceReset 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-draggingWhile the value is being dragged.
data-disabledWhen disabled.
data-bipolarWhen origin lies inside the range.
data-zoneThe 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-valueTravel of the value, from 0 to 1; on an endless knob, one per turn, without bounds.
--knob-angleRotation 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

On this page