ContinuousControl Primitive

Complete API reference for ContinuousControl - the primitive for building custom continuous value controls.

ContinuousControl is the primitive component for building custom continuous value controls. It handles all interaction logic, parameter management, and layout while you define the visual appearance.

API Reference

NameTypeDefaultRequiredDescription
viewControlComponent<P>YesThe visualization component that implements ControlComponentView
viewPropsPNoProps specific to the view component
valuenumberYesCurrent value of the control
onChange(event: AudioControlEvent<number>) => voidNoHandler for value changes
minnumberNoMinimum value (ad-hoc mode)
maxnumberNoMaximum value (ad-hoc mode)
stepnumberNoStep size for value adjustments (ad-hoc mode)
bipolarbooleanfalseNoWhether the component operates in bipolar mode
parameterContinuousParameterNoAudio Parameter definition (overrides ad-hoc props)
labelstringNoLabel displayed below the component
valueFormatter(value: number, parameterDef: AudioParameter) => string | undefinedNoCustom renderer for the value display
valueAsLabel"labelOnly" | "valueOnly" | "interactive"labelOnlyNoControls how label and value are displayed
interactionMode"drag" | "wheel" | "both"bothNoInteraction mode
interactionDirection"vertical" | "horizontal" | "circular" | "both"NoDirection of interaction
interactionSensitivitynumberNoSensitivity multiplier for interactions
viewBoxWidthUnitsnumberNoOverride viewBox width from view component
viewBoxHeightUnitsnumberNoOverride viewBox height from view component
labelHeightUnitsnumberNoOverride label height from view component
displayMode"scaleToFit" | "fill"scaleToFitNoHow the component scales within its container
labelMode"visible" | "hidden" | "none"visibleNoLabel display mode
labelPosition"above" | "below"belowNoLabel position relative to control
labelAlign"start" | "center" | "end"centerNoLabel horizontal alignment
classNamestringNoAdditional CSS classes
styleReact.CSSPropertiesNoAdditional inline styles

View Component Contract

Your view component must implement the ControlComponentView interface:

interface ControlComponentView {
  viewBox: { width: number; height: number };
  labelHeightUnits?: number; // Default: 20
  interaction?: {
    mode: "drag" | "wheel" | "both";
    direction: "vertical" | "horizontal" | "circular" | "both";
  };
}

Static Properties

Define these as static properties on your view component:

ViewComponentStatic.tsx
function MyCustomView({ normalizedValue }: ControlComponentViewProps) {
  // Your visualization
  return <g>{/* SVG content */}</g>;
}
 
// Required
MyCustomView.viewBox = { width: 100, height: 100 };
 
// Optional (provide defaults)
MyCustomView.labelHeightUnits = 15;
MyCustomView.interaction = {
  mode: "both",
  direction: "circular",
};

View Props

Your view component receives these props:

type ControlComponentViewProps<P = Record<string, unknown>> = {
  normalizedValue: number; // 0.0 to 1.0
  children?: React.ReactNode; // For center content (HTML overlay)
  className?: string;
  style?: React.CSSProperties;
  // ... any custom props from viewProps
} & P;

Basic Usage

BasicUsage.tsx
import { ContinuousControl, ControlComponentViewProps } from "@cutoff/audio-ui-react";
import { useState } from "react";
 
function SimpleKnobView({ normalizedValue }: ControlComponentViewProps) {
  const angle = normalizedValue * 270 - 135;
  const radians = (angle * Math.PI) / 180;
 
  return (
    <g>
      <circle cx="50" cy="50" r="45" fill="none" stroke="#ccc" strokeWidth="4" />
      <line
        x1="50"
        y1="50"
        x2={50 + 40 * Math.cos(radians)}
        y2={50 + 40 * Math.sin(radians)}
        stroke="currentColor"
        strokeWidth="3"
      />
    </g>
  );
}
 
SimpleKnobView.viewBox = { width: 100, height: 100 };
 
export default function BasicUsageExample() {
  const [value, setValue] = useState(0.5);
 
  return (
    <ContinuousControl
      view={SimpleKnobView}
      value={value}
      onChange={(e) => setValue(e.value)}
      min={0}
      max={1}
      label="Custom Knob"
    />
  );
}

Parameter Integration

Ad-Hoc Mode

Provide min, max, and optional step directly:

AdHocMode.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  min={20}
  max={20000}
  step={1}
  label="Frequency"
/>

Parameter Model Mode

Provide a full ContinuousParameter:

ParameterMode.tsx
import { createContinuousParameter } from "@cutoff/audio-ui-react";
 
const cutoffParam = createContinuousParameter({
  paramId: "cutoff",
  label: "Cutoff",
  min: 20,
  max: 20000,
  unit: "Hz",
  scale: "log",
  defaultValue: 1000,
});
 
<ContinuousControl
  view={MyView}
  parameter={cutoffParam}
  value={value}
  onChange={(e) => setValue(e.value)}
/>

Interaction Modes

Interaction Mode

Control which input methods are enabled:

InteractionMode.tsx
// Drag only
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionMode="drag"
/>
 
// Wheel only
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionMode="wheel"
/>
 
// Both (default)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionMode="both"
/>

Interaction Direction

Control how drag interactions map to value changes:

InteractionDirection.tsx
// Circular (for knobs)
<ContinuousControl
  view={KnobView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionDirection="circular"
/>
 
// Vertical (for vertical sliders)
<ContinuousControl
  view={VerticalSliderView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionDirection="vertical"
/>
 
// Horizontal (for horizontal sliders)
<ContinuousControl
  view={HorizontalSliderView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionDirection="horizontal"
/>

Interaction Sensitivity

Tune the responsiveness of interactions:

InteractionSensitivity.tsx
// Low sensitivity (fine control)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionSensitivity={0.001}
/>
 
// High sensitivity (coarse control)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  interactionSensitivity={0.01}
/>

Default sensitivity: 0.005 (200px throw for full range)

Custom View Props

Pass custom props to your view component via viewProps:

CustomViewProps.tsx
function CustomView({
  normalizedValue,
  customColor,
  customThickness,
}: ControlComponentViewProps<{
  customColor: string;
  customThickness: number;
}>) {
  return (
    <g>
      <circle
        cx="50"
        cy="50"
        r="45"
        fill="none"
        stroke={customColor}
        strokeWidth={customThickness}
      />
    </g>
  );
}
 
CustomView.viewBox = { width: 100, height: 100 };
 
<ContinuousControl
  view={CustomView}
  viewProps={{
    customColor: "#3b82f6",
    customThickness: 4,
  }}
  value={value}
  onChange={(e) => setValue(e.value)}
/>

Center Content (HTML Overlay)

Render HTML content (text, icons) as an overlay above the SVG:

CenterContent.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  htmlOverlay={
    <div style={{ fontSize: "20px", fontWeight: 700 }}>
      {formattedValue}
    </div>
  }
/>

Or use the children prop for a simpler API:

ChildrenContent.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
>
  <div style={{ fontSize: "20px", fontWeight: 700 }}>
    {formattedValue}
  </div>
  <div style={{ fontSize: "10px", opacity: 0.6 }}>dB</div>
</ContinuousControl>

Why HTML overlay? Safari has rendering bugs with SVG foreignObject in complex layouts. The overlay approach renders HTML outside the SVG for compatibility.

Value Display Modes

Control how the label and value are displayed:

ValueAsLabel.tsx
// Label only (default)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueAsLabel="labelOnly"
/>
 
// Value only
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueAsLabel="valueOnly"
/>
 
// Interactive (shows value during interaction)
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueAsLabel="interactive"
/>

Bipolar Mode

For controls centered around zero (pan, balance):

Bipolar.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  min={-100}
  max={100}
  bipolar={true}
/>

In bipolar mode:

  • Default value is center (0.5 normalized)
  • Visual rendering starts from center
  • Value indicators fill from center

Overriding ViewBox Dimensions

Override the default viewBox from your view component:

OverrideViewBox.tsx
<ContinuousControl
  view={MyView} // MyView.viewBox = { width: 100, height: 100 }
  viewBoxWidthUnits={150} // Override to 150
  viewBoxHeightUnits={150} // Override to 150
  value={value}
  onChange={(e) => setValue(e.value)}
/>

Layout Integration

ContinuousControl uses AdaptiveBox for layout. All AdaptiveBox props are supported:

LayoutProps.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  displayMode="fill" // or "scaleToFit"
  labelMode="visible" // or "hidden", "none"
  labelPosition="above" // or "below"
  labelAlign="start" // or "center", "end"
  labelHeightUnits={20} // Override label height
/>

Advanced Usage Patterns

Custom Formatter

Provide a custom value formatter:

CustomFormatter.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  valueFormatter={(value, paramDef) => {
    if (paramDef.unit === "Hz") {
      return frequencyFormatter(value);
    }
    return `${value.toFixed(1)}${paramDef.unit || ""}`;
  }}
/>

Double-Click Reset

ContinuousControl supports double-click to reset to default value when onChange is provided:

DoubleClickReset.tsx
<ContinuousControl
  view={MyView}
  value={value}
  onChange={(e) => setValue(e.value)}
  parameter={createContinuousParameter({
    // ...
    defaultValue: 1000, // Resets to this value on double-click
  })}
/>

Keyboard Support

Automatic keyboard support:

  • Arrow keys: Increment/decrement (clamped at min/max)
  • Home/End: Jump to min/max
  • Tab: Focus management

Performance Considerations

  • Memoization: Both ContinuousControl and view components are memoized
  • Event handlers: Use useRef for mutable state to avoid stale closures
  • Re-renders: Only re-renders when props change (value, viewProps, etc.)
  • Interaction: Global event listeners only attached during active drag

Type Exports

import {
  ContinuousControl,
  ContinuousControlComponentProps,
  ControlComponentView,
  ControlComponentViewProps,
} from "@cutoff/audio-ui-react";

Examples

See the View System and View Primitives guide for complete examples of building custom controls with ContinuousControl.

Next Steps