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

NameTypeDefaultRequiredDescription
valuenumber—RequiredCurrent value of the control
onChange(event: AudioControlEvent<number>) => void—OptionalHandler for value changes
minnumber—OptionalMinimum value (ad-hoc mode)
maxnumber—OptionalMaximum value (ad-hoc mode)
stepnumber1OptionalStep size for value adjustments
bipolarbooleanfalseOptionalWhether the component should operate in bipolar mode (centered at zero)
labelstring—OptionalLabel displayed below the component
variant"abstract" | "simplest" | "plainCap" | "iconCap"abstractOptionalVisual variant of the knob
thicknessnumber—OptionalThickness of the knob's stroke (normalized 0.0-1.0, maps to 1-20)
opennessnumber90OptionalOpenness of the ring in degrees (0-360º; 0º closed, 90º 3/4 open, 180º half-circle)
rotationnumber0OptionalOptional rotation angle offset in degrees
rotaryOverlaybooleanfalseOptionalWhether to use RotaryImage (true) or RadialImage (false) for iconCap overlay. Only applies when variant is "iconCap" and children is provided.
childrenReact.ReactNode—OptionalSVG 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"labelOnlyOptionalControls how the label and value are displayed
colorstring—OptionalComponent primary color - any valid CSS color value
roundnessnumber—OptionalRoundness for component corners (normalized 0.0-1.0)
adaptiveSizebooleanfalseOptionalWhether the component stretches to fill its container
size"xsmall" | "small" | "normal" | "large" | "xlarge"normalOptionalSize of the component
parameterContinuousParameter—OptionalAudio Parameter definition (Model). If provided, overrides min/max/step/label/bipolar from ad-hoc props
valueFormatter(value: number, parameterDef: AudioParameter) => string | undefined—OptionalCustom renderer for the value display
interactionMode"drag" | "wheel" | "both"bothOptionalInteraction mode
interactionSensitivitynumber—OptionalSensitivity of the control (normalized value change per pixel/unit)
interactionDirectionInteractionDirection—OptionalDirection 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-circle
  • 270º: 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:

ParameterModelExample.tsx
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