Knob
A rotary control component for continuous value adjustment with multiple visual variants and full theming support.
The Knob component provides a circular control for continuous value adjustment. It shares the same layout, parameter model, interaction, and accessibility as other vector components; see the Vector introduction.
- Multiple visual variants: abstract, simplest, plainCap, iconCap
- Full theming support: color, roundness, thickness customization
Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
value | number | — | Required | Current value of the control |
onChange | (event: AudioControlEvent<number>) => void | — | Optional | Handler for value changes |
min | number | — | Optional | Minimum value (ad-hoc mode) |
max | number | — | Optional | Maximum value (ad-hoc mode) |
step | number | 1 | Optional | Step size for value adjustments |
bipolar | boolean | false | Optional | Whether the component should operate in bipolar mode (centered at zero) |
label | string | — | Optional | Label displayed below the component |
variant | "abstract" | "simplest" | "plainCap" | "iconCap" | abstract | Optional | Visual variant of the knob |
thickness | number | — | Optional | Thickness of the knob's stroke (normalized 0.0-1.0, maps to 1-20) |
openness | number | 90 | Optional | Openness of the ring in degrees (0-360º; 0º closed, 90º 3/4 open, 180º half-circle) |
rotation | number | 0 | Optional | Optional rotation angle offset in degrees |
rotaryOverlay | boolean | false | Optional | Whether to use RotaryImage (true) or RadialImage (false) for iconCap overlay. Only applies when variant is "iconCap" and children is provided. |
children | React.ReactNode | — | Optional | SVG content to display as overlay in iconCap variant. Typically an icon component that will be rendered at the center of the knob. |
valueAsLabel | "labelOnly" | "valueOnly" | "interactive" | labelOnly | Optional | Controls how the label and value are displayed |
color | string | — | Optional | Component primary color - any valid CSS color value |
roundness | number | — | Optional | Roundness for component corners (normalized 0.0-1.0) |
adaptiveSize | boolean | false | Optional | Whether the component stretches to fill its container |
size | "xsmall" | "small" | "normal" | "large" | "xlarge" | normal | Optional | Size of the component |
parameter | ContinuousParameter | — | Optional | Audio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props |
valueFormatter | (value: number, parameterDef: AudioParameter) => string | undefined | — | Optional | Custom renderer for the value display |
interactionMode | "drag" | "wheel" | "both" | both | Optional | Interaction mode |
interactionSensitivity | number | — | Optional | Sensitivity of the control (normalized value change per pixel/unit) |
interactionDirection | InteractionDirection | — | Optional | Direction of the interaction |
Basic Usage
The Knob component requires a value and onChange handler. In ad-hoc mode, you provide min and max to define the value range:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function BasicKnob() {
const [value, setValue] = useState(50);
return (
<Knob
value={value}
onChange={(e) => setValue(e.value)}
min={0}
max={100}
size="large"
label="Volume"
/>
);
}
Variants
The Knob component supports four visual variants, each with a distinct appearance:
Simplest
A simplified version with minimal visual elements:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function SimplestVariant() {
const [value, setValue] = useState(0.5);
return (
<Knob
value={value}
onChange={(e) => setValue(e.value)}
variant="simplest"
label="Resonance"
size="large"
/>
);
}
Abstract
The default variant with a clean, minimal design. Displays the current value as text in the center:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function AbstractVariant() {
const [value, setValue] = useState(0.5);
return (
<Knob
value={value}
onChange={(e) => setValue(e.value)}
variant="abstract"
label="Cutoff"
min={0.2}
max={1}
size="large"
/>
);
}
PlainCap
A variant with a plain cap design:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function PlainCapVariant() {
const [value, setValue] = useState(0.5);
return (
<Knob
value={value}
onChange={(e) => setValue(e.value)}
variant="plainCap"
label="Gain"
size="large"
/>
);
}
IconCap
The iconCap variant allows you to display custom SVG content (typically icons) at the center of the knob. The icon can rotate with the knob value or remain static:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function IconCapExample() {
const [staticValue, setStaticValue] = useState(0);
const [rotaryValue, setRotaryValue] = useState(0);
return (
<div className="flex gap-8">
<Knob
variant="iconCap"
value={staticValue}
onChange={(e) => setStaticValue(e.value)}
min={-64}
max={64}
bipolar={true}
rotaryOverlay={false}
size="large"
label="Static icon"
>
<SquareWaveIcon />
</Knob>
<Knob
variant="iconCap"
value={rotaryValue}
onChange={(e) => setRotaryValue(e.value)}
min={-64}
max={64}
bipolar={true}
rotaryOverlay={true}
size="large"
label="Rotary Icon"
>
<SquareWaveIcon />
</Knob>
</div>
);
}
The rotaryOverlay prop controls whether the icon rotates with the knob value (true) or remains static (false). The icon inherits color via currentColor, so it adapts to light/dark mode automatically. The example above matches the LFO knob from the AudioUI playground (iconCap with SquareWaveIcon); the playground provides sine, triangle, square, and saw wave icons you can reuse.
Theming
Vector components share common theming features; see Theming Features in the Vector introduction for an overview. The Knob component supports theming through the color and roundness props, or via the global theme system:
Color Customization
You can customize the knob's primary color using the color prop:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function CustomColor() {
const [blueValue, setBlueValue] = useState(0.5);
const [redValue, setRedValue] = useState(0.5);
return (
<div className="flex gap-8">
<Knob
value={blueValue}
onChange={(e) => setBlueValue(e.value)}
color="#3b82f6"
size="large"
label="Blue Knob"
/>
<Knob
value={redValue}
onChange={(e) => setRedValue(e.value)}
color="#ef4444"
size="large"
label="Red Knob"
/>
</div>
);
}
Roundness
Control the roundness of the knob's corners:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function RoundnessExample() {
const [square, setSquare] = useState(0.5);
const [rounded, setRounded] = useState(0.5);
const [circle, setCircle] = useState(0.5);
return (
<div className="flex gap-8">
<Knob
value={square}
onChange={(e) => setSquare(e.value)}
roundness={0.0}
size="large"
label="Square"
/>
<Knob
value={rounded}
onChange={(e) => setRounded(e.value)}
roundness={0.5}
size="large"
label="Rounded"
/>
<Knob
value={circle}
onChange={(e) => setCircle(e.value)}
roundness={1.0}
size="large"
label="Fully Rounded"
/>
</div>
);
}
Note:
Value strip roundness: For the knob's value strips (the arc that shows the current value), roundness behaves in an on/off way: roundness={0.0} uses a square linecap, and any other value uses a round linecap. The correct behaviour has been achieved internally but does not pass our performance requirements, so the current binary behaviour is used.
Thickness
Adjust the stroke thickness of the knob ring:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function ThicknessExample() {
const [thin, setThin] = useState(0.5);
const [medium, setMedium] = useState(0.5);
const [thick, setThick] = useState(0.5);
return (
<div className="flex gap-8">
<Knob
value={thin}
onChange={(e) => setThin(e.value)}
thickness={0.2}
size="large"
label="Thin"
/>
<Knob
value={medium}
onChange={(e) => setMedium(e.value)}
thickness={0.5}
size="large"
label="Medium"
/>
<Knob
value={thick}
onChange={(e) => setThick(e.value)}
thickness={0.8}
size="large"
label="Thick"
/>
</div>
);
}
Ring Configuration
Openness
The openness prop controls how much of the ring is visible. It accepts values in degrees (0-360º):
0º: Closed ring (full circle)90º: 3/4 open (default)180º: Half-circle270º: 1/4 open
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function OpennessExample() {
const [closed, setClosed] = useState(0.5);
const [defaultOpen, setDefaultOpen] = useState(0.5);
const [half, setHalf] = useState(0.5);
return (
<div className="flex gap-8">
<Knob
value={closed}
onChange={(e) => setClosed(e.value)}
openness={0}
size="large"
label="Closed"
/>
<Knob
value={defaultOpen}
onChange={(e) => setDefaultOpen(e.value)}
openness={90}
size="large"
label="Default (90º)"
/>
<Knob
value={half}
onChange={(e) => setHalf(e.value)}
openness={180}
size="large"
label="Half Circle"
/>
</div>
);
}
Rotation
The rotation prop allows you to offset the starting angle of the ring:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function RotationExample() {
const [v1, setV1] = useState(0.5);
const [v2, setV2] = useState(0.5);
const [v3, setV3] = useState(0.5);
return (
<div className="flex gap-8">
<Knob
value={v1}
onChange={(e) => setV1(e.value)}
rotation={0}
size="large"
label="No Rotation"
/>
<Knob
value={v2}
onChange={(e) => setV2(e.value)}
rotation={45}
size="large"
label="45º Rotation"
/>
<Knob
value={v3}
onChange={(e) => setV3(e.value)}
rotation={90}
size="large"
label="90º Rotation"
/>
</div>
);
}
Value Display
The valueAsLabel prop controls how the label and value are displayed:
"labelOnly": Always shows the label (default)"valueOnly": Always shows the value"interactive": Shows label normally, but temporarily swaps to value during interaction
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function ValueDisplayExample() {
const [v1, setV1] = useState(0.5);
const [v2, setV2] = useState(0.5);
const [v3, setV3] = useState(0.5);
return (
<div className="flex gap-8">
<Knob
value={v1}
onChange={(e) => setV1(e.value)}
valueAsLabel="labelOnly"
size="large"
label="Label"
/>
<Knob
value={v2}
onChange={(e) => setV2(e.value)}
valueAsLabel="valueOnly"
size="large"
label="Volume"
/>
<Knob
value={v3}
onChange={(e) => setV3(e.value)}
valueAsLabel="interactive"
size="large"
label="Interactive"
/>
</div>
);
}
Bipolar Mode
In bipolar mode, the knob visualizes values relative to a center point rather than from minimum to maximum. This is useful for controls like pan or balance:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function BipolarExample() {
const [value, setValue] = useState(0);
return (
<Knob
value={value}
onChange={(e) => setValue(e.value)}
min={-100}
max={100}
bipolar={true}
size="large"
label="Pan"
/>
);
}
Value Formatting
You can customize how values are displayed using the valueFormatter prop. AudioUI provides built-in formatters for common audio parameters:
import { useState } from "react";
import {
Knob,
frequencyFormatter,
percentageFormatter,
} from "@cutoff/audio-ui-react";
export default function FormatterExample() {
const [frequency, setFrequency] = useState(1000);
const [mix, setMix] = useState(50);
return (
<div className="flex gap-4 items-center flex-wrap">
<Knob
value={frequency}
onChange={(e) => setFrequency(e.value)}
min={20}
max={20000}
label="Frequency"
size="large"
valueFormatter={(value) => frequencyFormatter(value)}
/>
<Knob
value={mix}
onChange={(e) => setMix(e.value)}
min={0}
max={100}
label="Mix"
size="large"
valueFormatter={(value, paramDef) => {
return percentageFormatter(value, paramDef.min, paramDef.max);
}}
/>
</div>
);
}Parameter Model
Instead of using ad-hoc props (min, max, label), you can provide a full parameter model via the parameter prop. This is useful when integrating with audio parameter systems:
import { Knob, createContinuousParameter } from "@cutoff/audio-ui-react";
const cutoffParam = createContinuousParameter({
paramId: "cutoff",
label: "Cutoff",
min: 20,
max: 20000,
unit: "Hz",
scale: "log",
defaultValue: 1000,
});
export default function ParameterModelExample() {
const [value, setValue] = useState(1000);
return (
<Knob
parameter={cutoffParam}
value={value}
onChange={(e) => setValue(e.value)}
/>
);
}Adaptive Sizing
The Knob component supports both fixed sizes and adaptive sizing. By default, the size of the component is driven by the Size System and their size attribute:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function SizingExample() {
const [v1, setV1] = useState(0.5);
const [v2, setV2] = useState(0.5);
const [v3, setV3] = useState(0.5);
const [v4, setV4] = useState(0.5);
return (
<div className="flex gap-4 items-center flex-wrap">
<Knob
value={v1}
onChange={(e) => setV1(e.value)}
size="small"
label="Small"
/>
<Knob
value={v2}
onChange={(e) => setV2(e.value)}
size="normal"
label="Normal"
/>
<Knob
value={v3}
onChange={(e) => setV3(e.value)}
size="large"
label="Large"
/>
<Knob
value={v4}
onChange={(e) => setV4(e.value)}
size="xlarge"
label="XLarge"
/>
</div>
);
}
Adaptive sizing allows the component to adapt to the size of its parent container, adjusting its dimensions intelligently without causing distortion (scaleToFit display mode). The fill display mode makes the component fill the entire container, potentially distorting it.
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function AdaptiveSize() {
const [wideValue, setWideValue] = useState(0.5);
const [tallValue, setTallValue] = useState(0.5);
const [distortedValue, setDistortedValue] = useState(0.5);
return (
<div className="flex gap-6 items-center flex-wrap">
<div className="w-48 h-16 border rounded-lg p-2">
<Knob
value={wideValue}
onChange={(e) => setWideValue(e.value)}
adaptiveSize={true}
label="Wide"
/>
</div>
<div className="w-24 h-48 border rounded-lg p-2">
<Knob
value={tallValue}
onChange={(e) => setTallValue(e.value)}
adaptiveSize={true}
label="Tall"
/>
</div>
<div className="w-32 h-20 border rounded-lg p-2">
<Knob
value={distortedValue}
onChange={(e) => setDistortedValue(e.value)}
adaptiveSize={true}
displayMode="fill"
label="Distorted"
/>
</div>
</div>
);
}
Interaction Modes
Control how users interact with the knob:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function InteractionExample() {
const [dragVal, setDragVal] = useState(0.5);
const [wheelVal, setWheelVal] = useState(0.5);
const [bothVal, setBothVal] = useState(0.5);
return (
<div className="flex gap-4 items-center flex-wrap">
<Knob
value={dragVal}
onChange={(e) => setDragVal(e.value)}
interactionMode="drag"
label="Drag Only"
size="large"
/>
<Knob
value={wheelVal}
onChange={(e) => setWheelVal(e.value)}
interactionMode="wheel"
label="Wheel Only"
size="large"
/>
<Knob
value={bothVal}
onChange={(e) => setBothVal(e.value)}
interactionMode="both"
label="Drag & Wheel"
size="large"
/>
</div>
);
}You can also adjust the sensitivity of interactions:
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function SensitivityExample() {
const [lowVal, setLowVal] = useState(0.5);
const [highVal, setHighVal] = useState(0.5);
return (
<div className="flex gap-4 items-center flex-wrap">
<Knob
value={lowVal}
onChange={(e) => setLowVal(e.value)}
interactionSensitivity={0.01}
label="Low Sensitivity"
size="large"
/>
<Knob
value={highVal}
onChange={(e) => setHighVal(e.value)}
interactionSensitivity={0.1}
label="High Sensitivity"
size="large"
/>
</div>
);
}Common Use Cases
Audio Filter Control
import { useState } from "react";
import { Knob, frequencyFormatter } from "@cutoff/audio-ui-react";
export default function FilterControl() {
const [cutoff, setCutoff] = useState(1000);
return (
<Knob
value={cutoff}
min={20}
max={20000}
label="Cutoff"
variant="abstract"
size="large"
valueFormatter={(value) => frequencyFormatter(value)}
unit="Hz"
scale="log"
onChange={(e) => setCutoff(e.value)}
/>
);
}Volume Control
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function VolumeControl() {
const [volume, setVolume] = useState(50);
return (
<Knob
value={volume}
min={0}
max={100}
label="Volume"
variant="plainCap"
size="large"
valueAsLabel="interactive"
unit="%"
onChange={(e) => setVolume(e.value)}
/>
);
}Pan Control
import { useState } from "react";
import { Knob } from "@cutoff/audio-ui-react";
export default function PanControl() {
const [pan, setPan] = useState(0);
return (
<Knob
value={pan}
min={-100}
max={100}
label="Pan"
variant="simplest"
size="large"
bipolar={true}
onChange={(e) => setPan(e.value)}
/>
);
}Next Steps
- Explore the Slider component for linear controls
- Learn about Button and CycleButton for discrete interactions
- Check out ImageKnob for custom bitmap-based knobs
- Visit the playground for interactive examples