Param Panel

Panels

Create a panel, mount it into your layout, and control it at runtime.

A panel is the root container for your controls. You create one with createPanel, then call builder methods on it to add sliders, folders, monitors, and everything else.

createPanel

function createPanel(config?: PanelConfig): ParamPanel;
import { createPanel } from "param-panel";

const pp = createPanel({
  id: "edge-detector-panel",
  title: "Edge Detector",
  position: "top-right",
  density: "compact",
});

PanelConfig

Every field is optional.

interface PanelConfig {
  /** Title shown at the top of the panel. */
  title?: string;

  /** Mount target. Defaults to document.body. */
  container?: HTMLElement;

  /** Initial position when mounted to the document. */
  position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";

  /**
   * Drag a floating panel by its header; on release it snaps to the nearest
   * window corner. Defaults to true for panels mounted to document.body.
   */
  draggable?: boolean;

  /** Initial collapsed state. Defaults to false. */
  collapsed?: boolean;

  /** Visual density. */
  density?: "compact" | "comfortable" | "touch";

  /** Additional root CSS class. */
  className?: string;

  /** Optional stable ID for serialization and diagnostics. */
  id?: string;
}

Mounting into your layout

By default a panel mounts to document.body, positions itself in a corner of the viewport, and is draggable by its header — drag it anywhere and it snaps to the nearest corner, staying pinned there when the window resizes. Pass draggable: false to disable this. To place the panel inside your own layout, pass a container; a contained panel is not draggable and ParamPanel does not force viewport positioning:

const sidebar = document.querySelector<HTMLElement>("#sidebar");

if (!sidebar) {
  throw new Error("Missing #sidebar");
}

const pp = createPanel({
  title: "Inspector",
  container: sidebar,
});

The ParamPanel interface

A panel is itself a control container (so it exposes every builder) and a control (so it has configure, destroy, and friends). It adds panel-level actions:

interface ParamPanel extends Control<PanelRuntimeConfig>, ControlContainer {
  setTitle(title: string): this;

  expand(): this;
  collapse(): this;
  setExpanded(expanded: boolean): this;
  isExpanded(): boolean;

  expandAll(): this;
  collapseAll(): this;

  refresh(): this;

  onChange(listener: PanelChangeListener): Unsubscribe;

  getState(): PanelState;
  setState(state: PanelState, options?: SetStateOptions): this;
}

Collapsing

pp.collapse();
pp.expand();
pp.setExpanded(window.innerWidth > 800);

expandAll() and collapseAll() recursively open or close every folder in the panel — handy for large inspectors.

Runtime configuration

Because the panel is a control, configure() accepts a PanelRuntimeConfig:

interface PanelRuntimeConfig {
  title?: string;
  disabled?: boolean;
  visible?: boolean;
  density?: "compact" | "comfortable" | "touch";
  className?: string;
}
pp.configure({ density: "touch", title: "Inspector (mobile)" });

Refresh

pp.refresh();

refresh() re-reads values and redraws. You only need it for getter/setter bindings that cannot notify ParamPanel about external mutations — reactive bindings update automatically.

Observing all changes

onChange observes every value-control change in the panel from one place — instead of subscribing to each control by hand. It is the seam that the persistence and URL adapters build on.

interface PanelChange {
  id: string; // path-based id of the changed control
  control: ValueControl<unknown, unknown>;
  value: unknown;
  details: ChangeDetails<unknown>;
}

type PanelChangeListener = (change: PanelChange) => void;

Only controls with a stable id are reported (the id is what makes a change addressable). For example, log every committed change:

const stop = pp.onChange((change) => {
  if (change.details.final) {
    console.log(`${change.id} = ${change.value}`);
  }
});

It returns an Unsubscribe function.

Adding controls

Every builder lives on the panel (and on folders). Full details are in the Controls and Visualizations sections, but here is the container surface at a glance:

interface ControlContainer {
  slider(config: SliderConfig): SliderControl;
  number(config: NumberConfig): NumberControl;
  checkbox(config: CheckboxConfig): CheckboxControl;
  text(config: TextConfig): TextControl;
  textarea(config: TextareaConfig): TextareaControl;
  select<T>(config: SelectConfig<T>): SelectControl<T>;
  color(config: ColorConfig): ColorControl;

  button(config: ButtonConfig): ButtonControl;
  folder(config: FolderConfig): FolderControl;
  row(config?: RowConfig): RowControl;
  tabs(config?: TabsConfig): TabsControl;

  monitor<T>(config: MonitorConfig<T>): MonitorControl<T>;
  graph(config: GraphConfig): GraphControl;
  canvas(config: CanvasConfig): CanvasControl;
}

A complete small panel

import { createPanel } from "param-panel";

const pp = createPanel({
  title: "Demo Parameters",
});

const speed = pp.slider({
  label: "Speed",
  value: 1,
  min: 0,
  max: 10,
  step: 0.1,
});

const enabled = pp.checkbox({
  label: "Enabled",
  value: true,
});

speed.subscribe((value) => {
  simulation.speed = value;
});

enabled.subscribe((value) => {
  simulation.enabled = value;
});

On this page