# 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.



```
from wigglystuff import Knob

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.


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