Tweakit

The panel API

tweaks() returns synchronously — panel.el to mount, panel.params for live values, and the methods on this page. On the code-split build, a panel that needs lazy modules builds its controls behind panel.ready; the element, params and methods work immediately.

Events

panel.on(fn) subscribes to every change; the callback gets the params bag and the key that changed (also available as params._last). It returns an unsubscribe function. Move things and watch the feed.

— change something —
const log = target.querySelector(".pa-log");
const lines = [];
const panel = tweaks("Events", {
  radius: [12, 0, 40, 1],
  mode: ["calm", "wild"],
  armed: false,
});
mount.append(panel.el);

panel.on((p, changed) => {
  lines.unshift(changed ? `${changed} → ${JSON.stringify(p[changed])}` : "(reset)");
  log.textContent = lines.slice(0, 8).join("\n");
});

mount is the panel's slot inside the stage; target is the demo surface it controls. In your own page you'd just document.body.append(panel.el).

set & reset

The panel is just another consumer of its own state: panel.set(key, value) moves a control programmatically (listeners fire, the UI follows), and panel.reset() restores every default — the same thing the toolbar's reset button does.

const tile = target.querySelector(".pa-tile");
const panel = tweaks("Remote", {
  hue: [260, 0, 360, 1],
  lift: [0, 0, 40, 1],
});
mount.append(panel.el);

const apply = (p) => {
  tile.style.background = `oklch(0.65 0.2 ${p.hue})`;
  tile.style.transform = `translateY(${-p.lift}px)`;
};
panel.on(apply);
apply(panel.params);

target.querySelector(".pa-shuffle").addEventListener("click", () => {
  panel.set("hue", Math.round(Math.random() * 360));
  panel.set("lift", Math.round(Math.random() * 40));
});
target.querySelector(".pa-restore").addEventListener("click", () => panel.reset());

render, disabled & hint

Any object-form control can carry a render predicate (show/hide on sibling values), a disabled flag or predicate (gray out + lock), and a hint tooltip behind an ⓘ marker. Flip “Glow” and watch the other two rows.

Glow me
const chip = target.querySelector(".pa-chip");
const panel = tweaks("Conditional", {
  glow: false,
  color: { type: "color", value: "#7C5CFF",
           render: (get) => get("glow"),          // hidden until glow is on
           hint: "Only rendered while glow is on" },
  strength: { type: "slider", value: 36, min: 0, max: 90,
              disabled: (get) => !get("glow") },  // grayed out instead
});
mount.append(panel.el);

const apply = (p) => {
  chip.style.boxShadow = p.glow ? `0 0 ${p.strength}px ${p.color}` : "none";
};
panel.on(apply);
panel.ready.then(() => apply(panel.params));

Undo & redo

Pass { undo: true } and the panel keeps a debounced history: ⌘Z / ⇧⌘Z work while the panel is hovered or focused (so it never hijacks the page's own undo), a continuous drag coalesces into one step, and panel.undo() / panel.redo() drive it from outside.

const card = target.querySelector(".ud-card");
const panel = tweaks("History", {
  width: [160, 60, 260, 1],
  round: [18, 0, 50, 1],
}, { undo: true });
mount.append(panel.el);

const apply = (p) => {
  card.style.width = `${p.width}px`;
  card.style.borderRadius = `${p.round}px`;
};
panel.on(apply);
apply(panel.params);

target.querySelector(".ud-undo-btn").addEventListener("click", () => panel.undo());
target.querySelector(".ud-redo-btn").addEventListener("click", () => panel.redo());

Persistence & presets

{ persist: "key" } saves values to localStorage (reload this page — the panel comes back as you left it) and unlocks presets: a presets menu appears in the toolbar, and savePreset / loadPreset / deletePreset / presets() drive the same store from code.

 
const tile = target.querySelector(".ps-tile");
const note = target.querySelector(".ps-note");
const panel = tweaks("Presets", {
  hue: [260, 0, 360, 1],
  size: [120, 40, 190, 1],
}, { persist: "tweakit-docs" });
mount.append(panel.el);

const apply = (p) => {
  tile.style.background = `oklch(0.6 0.18 ${p.hue})`;
  tile.style.width = `${p.size}px`;
  tile.style.height = `${p.size}px`;
};
panel.on(apply);
apply(panel.params);

target.querySelector(".ps-save").addEventListener("click", () => {
  panel.savePreset("mine");
  note.textContent = `presets: ${panel.presets().join(", ")}`;
});
target.querySelector(".ps-load").addEventListener("click", () => {
  note.textContent = panel.loadPreset("mine") ? "loaded “mine”" : "no preset saved yet";
});

Floating & draggable

Every panel is draggable by its header — an inline panel lifts into a floating layer on first drag (draggable: false pins it). Pass floating: true or { x, y } to start it floated, like a classic debug overlay.

const btn = target.querySelector(".fl-spawn");
let floater = null;
btn.addEventListener("click", () => {
  if (floater) { floater.el.remove(); floater = null; btn.textContent = "Spawn a floating panel"; return; }
  floater = tweaks("Floating", {
    note: "Drag my header",
    hue: [280, 0, 360, 1],
  }, { floating: { x: 24, y: 96 } });
  document.body.append(floater.el);
  btn.textContent = "Dismiss it";
});

The rest of the options

The full third argument, for reference:

OptionDoes
themetoken overrides — see Theming
persistlocalStorage key (or true to key by panel name); enables presets
filteradds a fuzzy search toggle to the toolbar (the field swaps in for the title)
floatingstart floated: true or { x, y }
draggableheader dragging — on by default, false pins
toolbarfalse for a bare panel (no copy / reset / presets)
undodebounced undo/redo history
onResetreplace the default reset behavior
onEditStart / onEditEndbracket a continuous drag — pause expensive work mid-scrub