Skip to content
Feeding a clanker? Grab this page as raw .md

Knob API#

Knob is a rotary control that looks like a knob on a synth or mixing console. Unlike CircularSlider (a full 360° ring), it sweeps a partial arc with a gap at the bottom by default, with a pointer showing the current position. Angles are measured in degrees clockwise from 12 o'clock, so the default start_angle=-135 / end_angle=135 gives the classic 270° sweep; pass a full start_angle=0, end_angle=360 for a gapless dial that wraps.

Ticks are configurable — ticks=N for evenly spaced marks, a list of values, or (value, label) pairs. Pass steps instead for a rotary selector that snaps to discrete detents (numbers or named positions). With midi=True the knob shows a "MIDI learn" button: click it, move a control on your hardware, and the next control-change message binds to the knob (Web MIDI, Chromium browsers). The binding is remembered in browser localStorage so it survives a restart.

See also: Fader for the linear console version, and CircularSlider for a full-ring dial.

Bases: AnyWidget

Audio-panel style rotary knob for selecting a single value.

Unlike :class:CircularSlider (a full 360° ring), the knob sweeps a partial arc with a gap at the bottom, like a synth or mixer knob. A pointer line shows the current position and you can drag it round. Angles are measured in degrees clockwise from 12 o'clock, so the default start_angle=-135 / end_angle=135 gives the classic 270° sweep.

The value range increases clockwise from start_angle (mapped to min_value) to end_angle (mapped to max_value). Pass a full 360° sweep (e.g. start_angle=0, end_angle=360) for a gapless full-circle knob that wraps at the seam.

Examples:

import marimo as mo
from wigglystuff import Knob

gain = mo.ui.anywidget(
    Knob(min_value=0, max_value=11, value=5, ticks=12, label="Gain")
)
gain

Create a Knob.

Parameters:

Name Type Description Default
value Optional[float]

Initial value; defaults to min_value. Clamped to range.

None
min_value float

Lower bound of the value range (at start_angle).

0.0
max_value float

Upper bound of the value range (at end_angle).

100.0
step float

Snap increment in value units (must be > 0).

1.0
start_angle float

Angle of min_value, in degrees clockwise from 12 o'clock. Default -135 (lower-left).

-135.0
end_angle float

Angle of max_value, in degrees clockwise from 12 o'clock. Default 135 (lower-right). Together with the default start_angle this is a 270° sweep.

135.0
ticks TickSpec

Tick/axis marks. None for none, an int N for N evenly spaced ticks, a list of values, or a list of (value, label) pairs.

None
steps Optional[Sequence[Any]]

Discrete detents to snap to (a rotary selector). Same shape as ticks — numbers or (value, label) pairs. When given, min_value/max_value are derived from the steps, the detents double as the ticks, and dragging snaps to the nearest one. Mutually exclusive with ticks.

None
size int

Diameter in pixels.

80
label str

Optional text label shown above the knob.

''
show_value bool

Render the current value as text below the knob.

True
color str

Optional CSS color for the value arc and pointer. Empty string uses the theme default.

''
midi bool

Show a "MIDI learn" button. Click it, then move a control on your hardware; the next control-change (CC) message binds to this knob and drives its value. Uses the Web MIDI API (Chromium browsers, secure context). Read the binding back via midi_cc / midi_channel / midi_device.

False
midi_cc int

Bind a control-change number (0-127) up front instead of learning it. -1 (default) leaves it unbound.

-1
midi_channel int

MIDI channel (0-15) for the binding, or -1 for any.

-1
midi_key str

localStorage key for persisting the learned binding across restarts. Defaults to label. Empty (no label either) disables persistence.

''
midi_scope Optional[str]

Namespace for the persisted binding, so different notebooks don't collide. Empty (default) uses the browser's URL path automatically; pass an explicit string to pin it (or to intentionally share a mapping across notebooks).

None
**kwargs Any

Forwarded to anywidget.AnyWidget.

{}
Source code in wigglystuff/knob.py
def __init__(
    self,
    value: Optional[float] = None,
    min_value: float = 0.0,
    max_value: float = 100.0,
    step: float = 1.0,
    start_angle: float = -135.0,
    end_angle: float = 135.0,
    ticks: TickSpec = None,
    steps: Optional[Sequence[Any]] = None,
    size: int = 80,
    label: str = "",
    show_value: bool = True,
    color: str = "",
    midi: bool = False,
    midi_cc: int = -1,
    midi_channel: int = -1,
    midi_key: str = "",
    midi_scope: Optional[str] = None,
    **kwargs: Any,
) -> None:
    """Create a Knob.

    Args:
        value: Initial value; defaults to ``min_value``. Clamped to range.
        min_value: Lower bound of the value range (at ``start_angle``).
        max_value: Upper bound of the value range (at ``end_angle``).
        step: Snap increment in value units (must be > 0).
        start_angle: Angle of ``min_value``, in degrees clockwise from 12
            o'clock. Default ``-135`` (lower-left).
        end_angle: Angle of ``max_value``, in degrees clockwise from 12
            o'clock. Default ``135`` (lower-right). Together with the
            default ``start_angle`` this is a 270° sweep.
        ticks: Tick/axis marks. ``None`` for none, an int ``N`` for ``N``
            evenly spaced ticks, a list of values, or a list of
            ``(value, label)`` pairs.
        steps: Discrete detents to snap to (a rotary selector). Same shape
            as ``ticks`` — numbers or ``(value, label)`` pairs. When given,
            ``min_value``/``max_value`` are derived from the steps, the
            detents double as the ticks, and dragging snaps to the nearest
            one. Mutually exclusive with ``ticks``.
        size: Diameter in pixels.
        label: Optional text label shown above the knob.
        show_value: Render the current value as text below the knob.
        color: Optional CSS color for the value arc and pointer. Empty
            string uses the theme default.
        midi: Show a "MIDI learn" button. Click it, then move a control on
            your hardware; the next control-change (CC) message binds to
            this knob and drives its value. Uses the Web MIDI API (Chromium
            browsers, secure context). Read the binding back via
            ``midi_cc`` / ``midi_channel`` / ``midi_device``.
        midi_cc: Bind a control-change number (0-127) up front instead of
            learning it. ``-1`` (default) leaves it unbound.
        midi_channel: MIDI channel (0-15) for the binding, or ``-1`` for any.
        midi_key: localStorage key for persisting the learned binding across
            restarts. Defaults to ``label``. Empty (no label either) disables
            persistence.
        midi_scope: Namespace for the persisted binding, so different
            notebooks don't collide. Empty (default) uses the browser's URL
            path automatically; pass an explicit string to pin it (or to
            intentionally share a mapping across notebooks).
        **kwargs: Forwarded to ``anywidget.AnyWidget``.
    """
    if midi_scope is None:
        midi_scope = ""
    if step <= 0:
        raise ValueError("step must be positive.")
    if start_angle == end_angle:
        raise ValueError("start_angle and end_angle must differ.")
    if abs(end_angle - start_angle) > 360:
        raise ValueError(
            "the sweep (end_angle - start_angle) cannot exceed 360 degrees."
        )

    if steps is not None:
        if ticks is not None:
            raise ValueError("`ticks` is mutually exclusive with `steps`.")
        step_values = normalize_steps(steps)
        min_value, max_value = step_values[0], step_values[-1]
        tick_dicts = normalize_ticks(steps, min_value, max_value)
        if value is None:
            value = step_values[0]
        else:
            value = min(step_values, key=lambda s: abs(s - float(value)))
    else:
        step_values = []
        if min_value >= max_value:
            raise ValueError("min_value must be less than max_value.")
        tick_dicts = normalize_ticks(ticks, min_value, max_value)
        if value is None:
            value = min_value
        value = clamp(float(value), min_value, max_value)

    super().__init__(
        value=float(value),
        min_value=float(min_value),
        max_value=float(max_value),
        step=float(step),
        start_angle=float(start_angle),
        end_angle=float(end_angle),
        ticks=tick_dicts,
        steps=step_values,
        size=size,
        label=label,
        show_value=show_value,
        color=color,
        midi=midi,
        midi_cc=midi_cc,
        midi_channel=midi_channel,
        midi_key=midi_key,
        midi_scope=midi_scope,
        **kwargs,
    )

Synced traitlets#

Traitlet Type Notes
value float Current value, mapped across the arc.
min_value float Lower bound (at start_angle).
max_value float Upper bound (at end_angle).
step float Snap increment in value units (continuous mode).
start_angle float Angle of min_value, degrees clockwise from 12 o'clock.
end_angle float Angle of max_value; a 360° span makes a full circle.
ticks list[dict] Normalized {"value", "label"} tick marks.
steps list[float] Discrete detents to snap to; empty means continuous.
size int Diameter in pixels.
label str Optional text label shown above the knob.
show_value bool Render the current value as text below the knob.
color str CSS color for the value arc and pointer. Empty follows the theme.
midi bool Show the MIDI-learn button and listen for control-change.
midi_cc int Bound control-change number (0-127), or -1 when unbound.
midi_channel int Bound MIDI channel (0-15), or -1 for any.
midi_device str Name of the bound MIDI input device.
midi_supported bool Whether the browser exposes Web MIDI (set from JS).
midi_learning bool Whether the knob is currently in learn mode.
midi_key str localStorage key for the persisted binding (defaults to label).
midi_scope str Namespace for the binding; empty uses the browser URL path.