External state integration
Bind controls to application-owned state — plain objects, reactive stores, or Nano Stores — without a synchronization loop.
ParamPanel controls own their value by default, but they can also bind to state your application already owns. There are two binding styles: getter/setter (pull) and reactive (push).
These are exactly the non-raw members of the
ValueSource<T> union that every
value control's value option accepts — a raw T, a GetterSetter<T>, or an
ExternalValue<T>.
Getter/setter binding
Pass a { get, set } object as the control's value. The control reads and
writes your state instead of holding its own:
const settings = {
threshold: 128,
};
const threshold = pp.slider({
label: "Threshold",
value: {
get: () => settings.threshold,
set: (value) => {
settings.threshold = value;
},
},
min: 0,
max: 255,
});A plain getter/setter cannot tell ParamPanel when the underlying value changes
from elsewhere. After mutating it externally, call
refresh():
settings.threshold = 180;
threshold.refresh();Structural reactive binding
If your state can notify subscribers, bind it structurally and skip the manual refresh entirely. Any value matching this shape works:
interface ExternalValue<T> {
get(): T;
set(value: T): void;
subscribe(listener: (value: T) => void): Unsubscribe;
}const thresholdStore = createStore(128);
const threshold = pp.slider({
label: "Threshold",
value: thresholdStore,
min: 0,
max: 255,
});Now synchronization is automatic and two-way:
- User changes the control → the store is updated.
- The store changes → the control UI is updated.
- No polling is required.
- The binding is removed when the control is destroyed.
No synchronization loops
ParamPanel guards against the classic feedback loop — control writes store, store
notifies control, control writes store again — using equality checks and internal
update guards. As long as your store skips no-op notifications (compares with
Object.is before emitting), updates settle in a single pass.
Nano Stores adapter
Nano Stores is supported through an optional adapter and is not a dependency of the core package:
import { atom } from "nanostores";
import { fromNanostore } from "param-panel/nanostores";
const $threshold = atom(128);
const threshold = pp.slider({
label: "Threshold",
value: fromNanostore($threshold),
min: 0,
max: 255,
});Other integrations
The same ExternalValue<T> shape adapts cleanly to Svelte stores, Vue refs,
Solid signals, React state, an RxJS BehaviorSubject, or a custom event emitter.
Write a thin adapter that exposes get/set/subscribe — no framework
dependency is ever added to core.
Reference: a minimal store
The createStore used above is just any object implementing the interface. Here
is a complete, dependency-free one:
function createStore<T>(initial: T) {
let value = initial;
const listeners = new Set<(value: T) => void>();
return {
get() {
return value;
},
set(next: T) {
if (Object.is(value, next)) return; // skip no-op — prevents loops
value = next;
for (const listener of listeners) listener(value);
},
subscribe(listener: (value: T) => void) {
listeners.add(listener);
listener(value);
return () => listeners.delete(listener);
},
};
}Bound to a control, changes flow both directions:
const thresholdStore = createStore(128);
const threshold = pp.slider({
label: "Threshold",
value: thresholdStore,
min: 0,
max: 255,
});
thresholdStore.set(180);
console.log(threshold.get()); // 180
threshold.set(200);
console.log(thresholdStore.get()); // 200