Developer Resources

Advanced Topics

Themes, events, data binding, and more — how to configure and extend ProControls for advanced use cases.

New here? See the Getting Started page for HTML setup, script tags, a minimal sketch, and the common options table.

Data Binding Saving Data Themes & Styles Events

Data Binding Experimental

Two-way reactive data binding between controls and JavaScript objects. When bound, controls and data stay synchronized automatically — no manual onChange handlers needed.

Experimental API: This feature is experimental and subject to change. Feedback is welcome at learnProControls@gmail.com.

Overview

The bind() function enables two-way reactive data binding with minimal boilerplate.

  • Control → Data: When the user moves a slider, the bound data updates automatically
  • Data → Control: When you modify the bound data, the control display updates automatically
  • No manual onChange: Everything stays in sync without callback wiring

Single-data controls

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.

Multi-data controls

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:

  • Panel: Match child control name properties
  • MultiSlider: Match names array
  • GridPad: Match names array (row names)
  • XYPad: Use x and y properties
  • RangeSlider: Use min and max properties
  • GridView: Bind to array; columns must match object property names

Example: Panel with multiple controls

const 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

Example: GridPad drum pattern

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);

How it works

Data binding uses Object.defineProperty to install reactive getters and setters directly on your data object:

  1. Initial sync: On bind(), each field is compared — data keys that exist are copied into the control; missing keys are filled from the control's current value
  2. Control → Data: When the user interacts with a control, the bound data updates immediately
  3. Data → Control: Writing directly to the data object (e.g. myData.volume = 0.8) updates the control display automatically
  4. No proxy needed: Reactive setters live on the original object, so your own variable reference always works

Browser support

Requires JavaScript Proxy support: Chrome 49+, Firefox 18+, Safari 10+, Edge 12+. Not supported: IE 11 and earlier.

API reference

control.bind(dataObject, fieldName)

ParameterDescription
dataObjectThe data object containing values to bind
fieldNameProperty 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);

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

Setup

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 }

Save to file

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

Load from file

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
    }
  }
});

Auto-save with localStorage

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));
localStorage limit: ~5 MB per origin, strings only. Fine for any number of control values. Not shared across browsers or devices — use JSON file export for that.

Themes & Styles

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

Built-in styles

black
cyan accent
stainless
navy accent · polished chrome
white
red accent
brushed
orange accent · grain
red
yellow accent
blue
yellow accent
yellow
red accent

proControlBackground()

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.

StyleTexture
blackFlat fill — #1a1a1a.
stainlessGaussian 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.
whiteFlat fill — #e8e8e8.
brushedThree layered sine-wave frequencies (fine grain, visible stroke bands, slow tonal drift) plus a directional sheen band — simulates brushed aluminum.
redFlat fill — #cc0000. Primary red background, darker red controls, yellow accent.
blueFlat fill — #0000cc. Primary blue background, darker blue controls, yellow accent.
yellowFlat fill — #e0d000. Warm yellow background, golden controls, red accent · dark text.

Runtime style switching

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 });
}

Per-control theme overrides

Pass a partial theme object to override individual color keys while keeping the rest of the active style.

KeyAffects
panelPanel background fill.
panelStrokePanel border and knob outline.
trackSlider groove / dial arc background.
capHighlightTop gradient color of the fader cap or knob.
capShadowBottom gradient color of the fader cap or knob.
capIndicatorAccent line / filled arc color / LED glow color.
labelControl label text color.
readoutReadout value text color.
readoutBgReadout pill background.
hoverGlowHover highlight fill (include alpha).
disabledOverlayOverlay when disabled: true.
fontp5 font object from loadFont(). null = canvas default.
new Dial({ label: 'MASTER', theme: { capIndicator: '#ffcc00' } });

Events

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.

Nothing to wire up. Just create controls — they start receiving events and drawing themselves immediately. Call .remove() when done.

How it works

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.

Coexisting with your own handlers

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.

Multi-touch: p5.js maps only the first touch to mouseX/mouseY, so only one control can be manipulated at a time. Two-finger gestures are unaffected.