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
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
view | ControlComponent<P> | — | Yes | The visualization component that implements ControlComponentView |
viewProps | P | — | No | Props specific to the view component |
value | number | — | Yes | Current value of the control |
onChange | (event: AudioControlEvent<number>) => void | — | No | Handler for value changes |
min | number | — | No | Minimum value (ad-hoc mode) |
max | number | — | No | Maximum value (ad-hoc mode) |
step | number | — | No | Step size for value adjustments (ad-hoc mode) |
bipolar | boolean | false | No | Whether the component operates in bipolar mode |
parameter | ContinuousParameter | — | No | Audio Parameter definition (overrides ad-hoc props) |
label | string | — | No | Label displayed below the component |
valueFormatter | (value: number, parameterDef: AudioParameter) => string | undefined | — | No | Custom renderer for the value display |
valueAsLabel | "labelOnly" | "valueOnly" | "interactive" | labelOnly | No | Controls how label and value are displayed |
interactionMode | "drag" | "wheel" | "both" | both | No | Interaction mode |
interactionDirection | "vertical" | "horizontal" | "circular" | "both" | — | No | Direction of interaction |
interactionSensitivity | number | — | No | Sensitivity multiplier for interactions |
viewBoxWidthUnits | number | — | No | Override viewBox width from view component |
viewBoxHeightUnits | number | — | No | Override viewBox height from view component |
labelHeightUnits | number | — | No | Override label height from view component |
displayMode | "scaleToFit" | "fill" | scaleToFit | No | How the component scales within its container |
labelMode | "visible" | "hidden" | "none" | visible | No | Label display mode |
labelPosition | "above" | "below" | below | No | Label position relative to control |
labelAlign | "start" | "center" | "end" | center | No | Label horizontal alignment |
className | string | — | No | Additional CSS classes |
style | React.CSSProperties | — | No | Additional 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:
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
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:
<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:
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:
// 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:
// 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:
// 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:
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:
<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:
<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:
// 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):
<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:
<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:
<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:
<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:
<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
useReffor 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
- Learn about DiscreteControl Primitive for discrete value controls
- Explore BooleanControl Primitive for boolean controls
- Check out View Primitives for building custom visualizations