CycleButton

A discrete interaction control that cycles through a set of options with support for custom visual content and multiple operation modes.

The CycleButton component provides a discrete interaction control that cycles through a set of options. 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
  • Four modes of operation: Ad-hoc (children), strict (parameter), hybrid, and options prop
  • Full theming support: color, roundness, thickness customization

Important: This control supports discrete interaction only (click to cycle, keyboard to step). It does not support continuous interaction (drag/wheel).

Props

NameTypeDefaultRequiredDescription
valuestring | number—OptionalCurrent value of the control (Controlled mode)
defaultValuestring | number—OptionalDefault value of the component (Uncontrolled mode)
onChange(event: AudioControlEvent<string | number>) => void—OptionalHandler for value changes
labelstring—OptionalLabel displayed below the component
optionsDiscreteOption[]—OptionalOption definitions for the parameter model (Ad-Hoc mode). Defines the parameter structure (value, label, midiValue). Does NOT provide visual content - use children (OptionView components) for that.
childrenReact.ReactNode—OptionalChild elements (OptionView components) for visual content mapping. Provides ReactNodes for rendering (icons, text, custom components). Does NOT define the parameter model - use options prop or parameter prop for that.
renderOption(option: { value: string | number; label: string }) => React.ReactNode—OptionalCustom renderer for options (used when parameter is provided but no children map exists)
thicknessnumber0.4OptionalThickness of the stroke (normalized 0.0-1.0, maps to 1-20). Used by rotary variant.
opennessnumber90OptionalOpenness of the ring in degrees (0-360º; 0º closed, 90º 3/4 open, 180º half-circle)
rotationnumber0OptionalOptional rotation angle offset in degrees
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
displayMode"scaleToFit" | "fill"scaleToFitOptionalLayout mode for the control
labelMode"none" | "hidden" | "visible"visibleOptionalVisibility of the label row
labelPosition"above" | "below"belowOptionalVertical position of the label relative to the control
labelAlign"start" | "center" | "end"centerOptionalHorizontal alignment of the label text
labelOverflow"ellipsis" | "abbreviate" | "auto"autoOptionalHow to handle label text overflow
labelHeightUnitsnumber—OptionalLabel height in the same units as SVG viewBox height
parameterDiscreteParameter—OptionalAudio Parameter definition (Model). If provided, overrides label/options from ad-hoc props
paramIdstring—OptionalIdentifier for the parameter this control represents
midiResolutionMidiResolution7OptionalMIDI resolution in bits (ad-hoc mode, ignored if parameter provided)
midiMapping"spread" | "sequential" | "custom"spreadOptionalMIDI mapping strategy (ad-hoc mode, ignored if parameter provided)
onClickReact.MouseEventHandler—OptionalClick event handler
onMouseDownReact.MouseEventHandler—OptionalMouse down event handler
onMouseUpReact.MouseEventHandler—OptionalMouse up event handler
onMouseEnterReact.MouseEventHandler—OptionalMouse enter event handler
onMouseLeaveReact.MouseEventHandler—OptionalMouse leave event handler
classNamestring—OptionalAdditional CSS classes
styleReact.CSSProperties—OptionalAdditional inline styles

Understanding Options vs Children

The CycleButton component distinguishes between two concepts:

  • options prop: Defines the parameter model (value, label, midiValue). Used for parameter structure.
  • children (OptionView components): Provides visual content (ReactNodes) for rendering. Used for display.

These serve different purposes and can be used together:

  • Use options when you have data-driven option definitions
  • Use children when you want to provide custom visual content (icons, styled text, etc.)
  • Use both: options for the model, children for visuals (matched by value)

Note:

Basic use: For basic use of CycleButton, and especially when using ad-hoc props (no parameter prop), prefer OptionView children. In that case the option set is inferred from the OptionViews: you declare each option once as <OptionView value="...">...</OptionView>, and the same children are used for display. Use the options prop or a full parameter when you need explicit parameter structure (e.g. data-driven options, MIDI mapping). For a full disambiguation of options vs OptionView, see Options vs OptionView.

Modes of Operation

The CycleButton component supports four modes of operation:

1. Ad-Hoc Mode (Options prop)

Model from options prop, visual from children (if provided) or default rendering:

import { CycleButton } from "@cutoff/audio-ui-react";
 
export default function OptionsPropModeExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      options={[
        { value: "sine", label: "Sine Wave" },
        { value: "square", label: "Square Wave" },
        { value: "sawtooth", label: "Sawtooth Wave" },
        { value: "triangle", label: "Triangle Wave" },
      ]}
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    />
  );
}
 

2. Ad-Hoc Mode (Children only)

Model inferred from OptionView children, visual from children:

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function ChildrenOnlyModeExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      defaultValue="sine"
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
 

3. Strict Mode (Parameter only)

Model from parameter prop, visual via renderOption callback. The following example uses children (OptionView); with the full library you would use createDiscreteParameter and the parameter prop:

import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function StrictModeExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}

4. Hybrid Mode (Parameter + Children)

Model from parameter prop, visual from children (matched by value):

import { CycleButton, OptionView, createDiscreteParameter } from "@cutoff/audio-ui-react";
 
const waveformParam = createDiscreteParameter({
  paramId: "waveform",
  label: "Waveform",
  options: [
    { value: "sine", label: "Sine Wave", midiValue: 0 },
    { value: "square", label: "Square Wave", midiValue: 1 },
    { value: "sawtooth", label: "Sawtooth Wave", midiValue: 2 },
    { value: "triangle", label: "Triangle Wave", midiValue: 3 },
  ],
  defaultValue: "sine",
});
 
export default function HybridModeExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      parameter={waveformParam}
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
 

Using OptionView

The OptionView component is used to provide visual content for each option. It accepts any ReactNode as children:

Text Content

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function TextOptionViewExample() {
  return (
    <CycleButton defaultValue="sine" label="Waveform" size="large">
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}

Icon Content

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function IconOptionViewExample() {
  return (
    <CycleButton defaultValue="sine" label="Waveform" size="large">
      <OptionView value="sine"><SineWaveIcon /></OptionView>
      <OptionView value="square"><SquareWaveIcon /></OptionView>
      <OptionView value="sawtooth"><SawWaveIcon /></OptionView>
      <OptionView value="triangle"><TriangleWaveIcon /></OptionView>
    </CycleButton>
  );
}

Custom Components

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CustomOptionViewExample() {
  return (
    <CycleButton defaultValue="sine" label="Waveform" size="large">
      <OptionView value="sine">
        <div className="flex items-center gap-1">
          <SineWaveIcon />
          <span>Sine</span>
        </div>
      </OptionView>
      <OptionView value="square">
        <div className="flex items-center gap-1">
          <SquareWaveIcon />
          <span>Square</span>
        </div>
      </OptionView>
    </CycleButton>
  );
}
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function OptionViewExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} label="Waveform" size="large">
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
      <OptionView value="sawtooth">Saw</OptionView>
      <OptionView value="triangle">Tri</OptionView>
    </CycleButton>
  );
}
 

Theming

Vector components share common theming features; see Theming Features in the Vector introduction for an overview. The CycleButton component supports theming through the color and roundness props:

Color Customization

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonColorExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} color="#3b82f6" label="Waveform" size="large">
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
    </CycleButton>
  );
}
 

Roundness

Control the roundness of the knob ring:

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonRoundnessExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} roundness={0.0} label="Square" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} roundness={1.0} label="Fully Rounded" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </>
  );
}
 

Thickness

Adjust the stroke thickness:

import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonThicknessExample() {
  const [thinVal, setThinVal] = useState("sine");
  const [thickVal, setThickVal] = useState("sine");
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <CycleButton
        value={thinVal}
        onChange={(e) => setThinVal(e.value)}
        thickness={0.2}
        label="Thin"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton
        value={thickVal}
        onChange={(e) => setThickVal(e.value)}
        thickness={0.8}
        label="Thick"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </div>
  );
}

Ring Configuration

Openness

Control how much of the ring is visible:

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonOpennessExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} openness={0} label="Closed" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} openness={90} label="Default (90º)" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} openness={180} label="Half Circle" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </>
  );
}
 

Rotation

Offset the starting angle of the ring:

import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonRotationExample() {
  const [zeroVal, setZeroVal] = useState("sine");
  const [rot45Val, setRot45Val] = useState("sine");
 
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <CycleButton
        value={zeroVal}
        onChange={(e) => setZeroVal(e.value)}
        rotation={0}
        label="No Rotation"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton
        value={rot45Val}
        onChange={(e) => setRot45Val(e.value)}
        rotation={45}
        label="45º Rotation"
        size="large"
      >
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </div>
  );
}

Adaptive Sizing

The CycleButton 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 { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonSizingExample() {
  const [waveform, setWaveform] = useState("sine");
  return (
    <>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="small" label="Small">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="normal" label="Normal">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="large" label="Large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} size="xlarge" label="XLarge">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
      </CycleButton>
    </>
  );
}
 

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 { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CycleButtonAdaptiveSize() {
  const [wideValue, setWideValue] = useState("sine");
  const [tallValue, setTallValue] = useState("sine");
  const [distortedValue, setDistortedValue] = useState("sine");
 
  return (
    <div className="flex gap-6 items-center flex-wrap">
      <div className="w-48 h-16 border rounded-lg p-2">
        <CycleButton
          value={wideValue}
          onChange={(e) => setWideValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Wide"
        >
          <OptionView value="sine">Sine</OptionView>
          <OptionView value="square">Square</OptionView>
        </CycleButton>
      </div>
      <div className="w-24 h-48 border rounded-lg p-2">
        <CycleButton
          value={tallValue}
          onChange={(e) => setTallValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Tall"
        >
          <OptionView value="sine">Sine</OptionView>
          <OptionView value="square">Square</OptionView>
        </CycleButton>
      </div>
      <div className="w-32 h-20 border rounded-lg p-2">
        <CycleButton
          value={distortedValue}
          onChange={(e) => setDistortedValue(e.value)}
          adaptiveSize={true}
          displayMode="fill"
          label="Distorted"
        >
          <OptionView value="sine">Sine</OptionView>
          <OptionView value="square">Square</OptionView>
        </CycleButton>
      </div>
    </div>
  );
}
 

Controlled vs Uncontrolled

The CycleButton supports both controlled and uncontrolled modes:

Controlled Mode

import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function ControlledModeExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
    </CycleButton>
  );
}

Uncontrolled Mode

import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function UncontrolledModeExample() {
  return (
    <CycleButton
      defaultValue="sine"
      onChange={(e) => console.log("Selected:", e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine">Sine</OptionView>
      <OptionView value="square">Square</OptionView>
    </CycleButton>
  );
}

MIDI Integration

When using the options prop or parameter model, you can specify MIDI values for each option:

import { useState } from "react";
import { CycleButton } from "@cutoff/audio-ui-react";
 
export default function MIDIIntegrationExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      options={[
        { value: "sine", label: "Sine Wave", midiValue: 0 },
        { value: "square", label: "Square Wave", midiValue: 1 },
        { value: "sawtooth", label: "Sawtooth Wave", midiValue: 2 },
        { value: "triangle", label: "Triangle Wave", midiValue: 3 },
      ]}
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    />
  );
}

Common Patterns

Waveform Selector

A typical waveform selector for synthesizers:

import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function WaveformSelectorExample() {
  const [waveform, setWaveform] = useState("sine");
 
  return (
    <CycleButton
      value={waveform}
      onChange={(e) => setWaveform(e.value)}
      label="Waveform"
      size="large"
    >
      <OptionView value="sine"><SineWaveIcon /></OptionView>
      <OptionView value="square"><SquareWaveIcon /></OptionView>
      <OptionView value="sawtooth"><SawWaveIcon /></OptionView>
      <OptionView value="triangle"><TriangleWaveIcon /></OptionView>
    </CycleButton>
  );
}

Filter Type Selector

A filter type selector:

import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function FilterTypeSelectorExample() {
  const [filterType, setFilterType] = useState("lowpass");
 
  return (
    <CycleButton
      value={filterType}
      onChange={(e) => setFilterType(e.value)}
      label="Filter Type"
      size="large"
    >
      <OptionView value="lowpass">LP</OptionView>
      <OptionView value="highpass">HP</OptionView>
      <OptionView value="bandpass">BP</OptionView>
      <OptionView value="notch">Notch</OptionView>
    </CycleButton>
  );
}
import { useState } from "react";
import { CycleButton, OptionView } from "@cutoff/audio-ui-react";
 
export default function CommonPatternExample() {
  const [waveform, setWaveform] = useState("sine");
  const [filterType, setFilterType] = useState("lowpass");
  return (
    <div className="flex gap-4 items-center flex-wrap">
      <CycleButton value={waveform} onChange={(e) => setWaveform(e.value)} label="Waveform" size="large">
        <OptionView value="sine">Sine</OptionView>
        <OptionView value="square">Square</OptionView>
        <OptionView value="sawtooth">Saw</OptionView>
        <OptionView value="triangle">Tri</OptionView>
      </CycleButton>
      <CycleButton value={filterType} onChange={(e) => setFilterType(e.value)} label="Filter Type" size="large">
        <OptionView value="lowpass">LP</OptionView>
        <OptionView value="highpass">HP</OptionView>
        <OptionView value="bandpass">BP</OptionView>
        <OptionView value="notch">Notch</OptionView>
      </CycleButton>
    </div>
  );
}
 

Common Use Cases

The Waveform Selector and Filter Type Selector examples above (in Common Patterns) show typical use cases with live demos.

Next Steps