Themes, events, data binding, and more — how to configure and extend ProControls for advanced use cases.
Two-way reactive data binding between controls and JavaScript objects. When bound, controls and data stay synchronized automatically — no manual onChange handlers needed.
The bind() function enables two-way reactive data binding with minimal boilerplate.
Interactive controls with a single value use explicit field binding:
const config = { volume: 0.5 };
const slider = new AnalogSlider({ label: 'Volume', min: 0, max: 1 });
slider.bind(config, 'volume');
Supported controls: AnalogSlider, Selector, Switch, IconButton, SliderSelector, VUMeter, StatusPanel, ConsolePanel, TimeGraphPanel.
Container and array-based controls automatically match field names:
const data = { bass: 0.3, mid: 0.5, treble: 0.7 };
const multiSlider = new MultiSlider({ names: ['bass', 'mid', 'treble'] });
multiSlider.bind(data); // Auto-matches property names
For multi-data controls, property names in your data object must match the control's internal field names:
name propertiesnames arraynames array (row names)x and y propertiesmin and max propertiesconst audioConfig = {
masterVolume: 0.7,
bassEQ: 0.4,
trebleEQ: 0.6
};
let panel = new Panel({ width: 300, height: 300 });
let volume = new AnalogSlider({
name: 'masterVolume', // Must match audioConfig key
label: 'Master Volume'
});
panel.add(volume);
let bass = new AnalogSlider({
name: 'bassEQ', // Must match audioConfig key
label: 'Bass EQ'
});
panel.add(bass);
panel.bind(audioConfig); // Bind panel - auto-matches all children
const drumPattern = {
kick: [1, 0, 1, 0, 1, 0, 1, 0],
snare: [0, 1, 0, 1, 0, 1, 0, 1],
hihat: [1, 1, 1, 1, 1, 1, 1, 1]
};
let gridPad = new GridPad({
names: ['kick', 'snare', 'hihat'], // Must match pattern keys
rows: 3,
cols: 8
});
gridPad.bind(drumPattern);
Data binding uses Object.defineProperty to install reactive getters and setters directly on your data object:
bind(), each field is compared — data keys that exist are copied into the control; missing keys are filled from the control's current valuemyData.volume = 0.8) updates the control display automaticallyRequires JavaScript Proxy support: Chrome 49+, Firefox 18+, Safari 10+, Edge 12+. Not supported: IE 11 and earlier.
control.bind(dataObject, fieldName)
| Parameter | Description |
|---|---|
| dataObject | The data object containing values to bind |
| fieldName | Property name to bind to (required for single-data controls, optional for multi-data) |
// Single-data
slider.bind(data, 'volume');
// Multi-data
panel.bind(data);
multiSlider.bind(data);
Because Data Binding keeps your data object in sync at all times, saving state is just a matter of serializing that object — no manual scraping of control values needed.
Bind a panel to a plain object. On bind(), the object is populated with each control's current value, and stays in sync as the user interacts.
let myData = {};
let panel = new Panel({ label: 'Synth' });
panel.add(new AnalogSlider({ name: 'cutoff', label: 'Cutoff', min: 20, max: 20000 }));
panel.add(new AnalogSlider({ name: 'resonance', label: 'Resonance' }));
panel.add(new Selector({ name: 'waveform', options: ['Sine', 'Saw', 'Square'] }));
panel.bind(myData); // myData is now { cutoff, resonance, waveform }
p5.js has a built-in saveJSON() that triggers a browser download of the data object as a .json file.
saveJSON(myData, 'preset'); // downloads preset.json
Use createFileInput() to let the user pick a JSON file. Writing the loaded values back into myData triggers the reactive setters, so controls update automatically — no manual sync required.
let loadBtn = createFileInput((file) => {
if (file.type === 'application' && file.subtype === 'json') {
for (const [key, val] of Object.entries(file.data)) {
myData[key] = val; // reactive setter updates each control
}
}
});
To persist state across page refreshes, save to localStorage whenever a control changes and restore on startup.
let myData = {};
let panel = new Panel({ label: 'Synth' });
panel.add(new AnalogSlider({ name: 'cutoff', label: 'Cutoff' }));
panel.add(new AnalogSlider({ name: 'resonance', label: 'Resonance' }));
panel.bind(myData);
// Restore saved values if they exist
const saved = localStorage.getItem('synthPreset');
if (saved) {
for (const [key, val] of Object.entries(JSON.parse(saved))) {
myData[key] = val;
}
}
// Persist on every change
panel.onChange = () => localStorage.setItem('synthPreset', JSON.stringify(myData));
Set ControlStyle before creating controls. The style is captured at construction time, so controls built before a change keep their original look.
ControlStyle = 'brushed'; // before any new controls
Use instead of p5's background(). Fills the canvas with the style color. 'brushed' and 'stainless' also draw per-row texture overlays via the native Canvas 2D API.
| Style | Texture |
|---|---|
| black | Flat fill — #1a1a1a. |
| stainless | Gaussian highlight profile: sharp specular band near the top third, soft secondary reflection below center, edge darkening, and a thin pure-white specular line at the peak. |
| white | Flat fill — #e8e8e8. |
| brushed | Three layered sine-wave frequencies (fine grain, visible stroke bands, slow tonal drift) plus a directional sheen band — simulates brushed aluminum. |
| red | Flat fill — #cc0000. Primary red background, darker red controls, yellow accent. |
| blue | Flat fill — #0000cc. Primary blue background, darker blue controls, yellow accent. |
| yellow | Flat fill — #e0d000. Warm yellow background, golden controls, red accent · dark text. |
Rebuild controls with proControlReset() when switching styles. The Live Demo uses a Selector to cycle all styles.
const STYLES = ['black', 'white', 'brushed', 'red'];
let currentStyle = 'black';
function buildControls() {
proControlReset();
ControlStyle = currentStyle;
mySlider = new AnalogSlider({ value: mySlider?.value ?? 0.5 });
}
Pass a partial theme object to override individual color keys while keeping the rest of the active style.
| Key | Affects |
|---|---|
| panel | Panel background fill. |
| panelStroke | Panel border and knob outline. |
| track | Slider groove / dial arc background. |
| capHighlight | Top gradient color of the fader cap or knob. |
| capShadow | Bottom gradient color of the fader cap or knob. |
| capIndicator | Accent line / filled arc color / LED glow color. |
| label | Control label text color. |
| readout | Readout value text color. |
| readoutBg | Readout pill background. |
| hoverGlow | Hover highlight fill (include alpha). |
| disabledOverlay | Overlay when disabled: true. |
| font | p5 font object from loadFont(). null = canvas default. |
new Dial({ label: 'MASTER', theme: { capIndicator: '#ffcc00' } });
All mouse, wheel, touch events, and rendering are handled automatically by the library. No event functions and no draw loop are needed in your sketch. touch-action: none is applied to the canvas automatically to prevent iOS scroll conflicts.
.remove() when done.Each control self-registers on construction into a shared array. Two p5.prototype.registerMethod hooks handle everything: a 'pre' hook fires at the start of every frame to dispatch mouse/wheel/touch events; a 'post' hook fires at the end of every frame and draws any control that wasn't already drawn explicitly that frame. This means you can still call c.draw() manually for z-order control — the post hook simply skips controls already rendered.
Registered library methods and sketch-level event functions both fire independently — they don't conflict. You can still define mousePressed() in your sketch for other logic.
mouseX/mouseY, so only one control can be manipulated at a time. Two-finger gestures are unaffected.