npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tweaks

v0.1.8

Published

A GUI library with an immediate mode API to inspect, tweak and save values back to your code

Readme

Tweaks

A fully featured, reactive GUI library with an Immediate Mode API to easily tweak object values and save changes back to your code.

Think of it as a mix between lil-gui / Tweakpane and Dear ImGui.

I made this to fix a few pain points with existing JavaScript GUI libraries:

  • Reactive by default: the data is the single source of truth. The GUI always reflects the current values, no bindings or callbacks needed to sync
  • UI state persists across reloads: folder open/close states, panel positions, scroll positions are all remembered automatically.
  • Save changes back to your code: tweak values in the browser, hit save, and the changes are written directly to a state file. No more copy/pasting values from the GUI into your code.

Getting started

npm install tweaks
import { GUI } from 'tweaks';

const obj = { speed: 1, gravity: 9.8 };

GUI.render(() => {
  GUI.Number(obj, 'speed', 0, 100, 0.1);
  GUI.Number(obj, 'gravity', 0, 20, 0.1);
  if (GUI.Button('Reset')) {
    obj.speed = 1;
    obj.gravity = 9.8;
  }
});

TWEAKS

TWEAKS lets you define typed variables that sync with the GUI and get saved to a JS file in your project. Change a value in the browser, hit save, and it's written to your code.

import { TWEAKS, GUI, Float, Int, Bool, Color } from 'tweaks';
import { tweaks } from './tweaks.js';

TWEAKS.sync(tweaks);

const params = TWEAKS.group('Particles', {
  count: Int(200, 10, 1000),
  speed: Float(1.5, 0.1, 10, 0.1),
  visible: Bool(true),
  color: Color('#4D9CFF'),
});

GUI.render(() => {
  GUI.Tweaks(params);
});

Running the server

Start the dev server:

npx tweaks

This starts an HTTP server, auto-creates a tweaks.js state file if it doesn't exist, and opens the browser.

Options:

npx tweaks 3000              # Custom port
npx tweaks ./mystate.js      # Custom state file path
npx tweaks 3000 ./mystate.js # Both
npx tweaks --no-open         # Don't open browser

State file

The server automatically creates and manages a tweaks.js file:

/** @type {{ __path__: string, data: Object }} */
export const tweaks = {
  __path__: '/tweaks.js',
  data: {},
};

Tweaks values are written to this file when you click SAVE in the GUI (or press Cmd+S). Import it and pass it to TWEAKS.sync():

import { TWEAKS } from 'tweaks';
import { tweaks } from './tweaks.js';

TWEAKS.sync(tweaks);

For browser-only persistence (no server), you can use the localStorage mode:

TWEAKS.sync('localStorage');

Registering tweaks

Create a group with a schema:

const myTweaks = TWEAKS.group('My Tweaks', {
  speed: Float(1, 0, 10, 0.1),
  count: Int(5, 0, 100),
  name: Str('default'),
  active: Bool(true),
  tint: Color('#ff6600'),
});

Nested plain objects automatically create collapsible folders:

const params = TWEAKS.group('Config', {
  speed: Float(1, 0, 10),
  physics: {
    gravity: Float(9.8, 0, 20),
    friction: Float(0.5, 0, 1),
  },
  name: Str('hello'),
});

params.physics.gravity  // 9.8
params.physics.friction // 0.5

Or register properties individually:

const myTweaks = TWEAKS.group('My Tweaks');

myTweaks.Float('speed', 1, 0, 10, 0.1);
myTweaks.Int('count', 5, 0, 100);
myTweaks.Str('name', 'default');
myTweaks.Bool('active', true);
myTweaks.Color('tint', '#ff6600');

Or with group.add():

const myTweaks = TWEAKS.group('My Tweaks');

myTweaks.add('speed', Float(1, 0, 10, 0.1));
myTweaks.add('count', Int(5, 0, 100));

You can then read or write values directly on the group like a regular JS Object:

console.log(myTweaks.speed); // 1
myTweaks.speed = 5;

Built-in types

| Type | Factory | Parameters | Widget | |------|---------|------------|--------| | Float | Float(default, min, max, step) | All optional | Number input | | Int | Int(default, min, max) | All optional, step forced to 1 | Number input | | Str | Str(default) | Optional | Text input | | Bool | Bool(default) | Optional | Checkbox | | Color | Color(default) | Optional | Color picker |

Rendering tweaks

GUI.Tweaks(group) renders all registered properties in a collapsible folder and adds SAVE, REVERT, RESET buttons to the parent panel:

GUI.render(() => {
  if (GUI.Tweaks(myTweaks)) {
    // A value changed
  }
});

Or as a regular JS Object properties:

GUI.render(() => {
  GUI.Number(myTweaks, 'speed');
  GUI.Number(myTweaks, 'count');
});

Custom types

Register a new type with TWEAKS.registerType(name, defaultValue, constructorFn). The constructor function renders the GUI for this type and returns true when the value changes. Extra parameters are available as prop.n1 through prop.n7.

The constructor receives a prop object where prop.value holds the current value and prop.n1 through prop.n7 hold the extra parameters from the factory. It must return true when the value changes.

Since prop.value here is an object, it can be passed directly as the target for sub-widgets:

// Value is { from, to, midpoint }, rendered as a CSS linear-gradient string
const defaultGradient = { from: '#ff0000', to: '#0000ff', midpoint: 50 };

const Gradient = TWEAKS.registerType('Gradient', defaultGradient, (prop, labelKey) => {
  let changed = false;
  GUI.Label(labelKey);
  GUI.BeginGroup(labelKey, 'div.row');
    if (GUI.ColorInput(prop.value, 'from')) changed = true;
    if (GUI.SliderInput(prop.value, 'midpoint', 0, 100, 1)) changed = true;
    if (GUI.ColorInput(prop.value, 'to')) changed = true;
  GUI.EndGroup(labelKey);
  return changed;
});

const g = TWEAKS.group('Theme', {
  sky: Gradient({ from: '#0a1628', to: '#4D9CFF', midpoint: 60 }),
});

// g.sky.from = '#0a1628', g.sky.to = '#4D9CFF', g.sky.midpoint = 60
const css = `linear-gradient(${g.sky.from}, ${g.sky.to} ${g.sky.midpoint}%)`;

The factory returned by registerType accepts (defaultValue, n1, n2, ...n7). A method with the same name is also added to all groups:

g.Gradient('sky', { from: '#0a1628', to: '#4D9CFF', midpoint: 60 });

GUI

Immediate mode

The GUI follows the immediate mode pattern: widgets are declared every frame inside a render callback, not created once and bound to events. There is no setup, no binding, no callback registration. Your data is the single source of truth. Widgets read from it and write to it directly.

Input widgets return true the frame their value changes. Buttons return true the frame they are clicked. This means your logic and your UI live in the same place:

GUI.render(() => {
  if (GUI.Slider(obj, 'volume', 0, 1, 0.01)) {
    audio.setVolume(obj.volume); // runs only when the slider moves
  }
  if (GUI.Button('Mute')) {
    obj.volume = 0; // runs only on click
  }
});

The library handles DOM creation, updates, and cleanup automatically. Widgets that are not declared in a frame are removed from the DOM. Widgets that reappear are recreated.

Widget identity

Widgets are identified by their target + labelKey combination within their parent container. If you need to display the same variable twice with the same widget type in the same folder, use a different label or id to make each widget unique:

// Won't work: same target + labelKey in the same parent, only one widget appears
GUI.Number(obj, 'x', 0, 100);
GUI.Number(obj, 'x', 0, 100);

// Works: different labels make them unique
GUI.Number(obj, 'x::"Position X"', 0, 100);
GUI.Number(obj, 'x::"Duplicate X"', 0, 100);

Minimal setup

<script type="module" src="app.js"></script>
import { GUI } from 'tweaks';

const obj = { x: 0 };

GUI.render(() => {
  if (GUI.BeginPanel('My Panel')) {
    GUI.Number(obj, 'x', 0, 100, 1);
    GUI.EndPanel();
  }
});

GUI.render(callback) registers a function that runs every frame. All widget calls go inside this callback.

For manual control of the render loop:

function loop() {
  GUI.beginRender();
  // widget calls here
  GUI.endRender();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

Binding variables to widgets

Widgets take a target object and a labelKey string. The widget reads and writes target[key]:

const obj = { x: 10, name: 'hello' };
GUI.Number(obj, 'x', 0, 100);    // reads/writes obj.x
GUI.Text(obj, 'name');           // reads/writes obj.name

Arrays work with numeric keys:

const arr = [10, 20, 30];
GUI.Number(arr, 0, 0, 100);      // reads/writes arr[0]

LabelKey syntax

The labelKey parameter controls the property key, display label, CSS classes, HTML id, and attributes. Without ::, the string is both the key and the label:

GUI.Text(obj, 'name');           // key='name', label='name'

Add :: to separate the key from modifiers:

key::"Custom Label"#id.class[attribute]

| Part | Description | |------|-------------| | key | Property name on the target object | | :: | Separator between key and modifiers | | "Label" | Custom display label. "" for empty label | | #id | Custom HTML id, also used with SetParent('#id') | | .class | CSS class(es) | | [attr] | HTML attribute (e.g. [disabled]) |

Examples:

GUI.Text(obj, 'prop::"Display Name"');
GUI.Text(obj, 'prop::""');                          // empty label
GUI.Text(obj, 'prop::.color_red');                  // colored
GUI.Text(obj, 'prop::"Name".color_red');            // label + color
GUI.Text(obj, 'prop::"Name"#myId.color_red');       // label + id + color
GUI.Slider(obj, 'x::"Position"[disabled]', 0, 100); // disabled
GUI.Number(arr, '0::"First item"', 0, 100);         // numeric key

Available color classes: .color_red, .color_corail, .color_orange, .color_yellow, .color_citrus, .color_lime, .color_green, .color_turquoise, .color_cyan, .color_sky, .color_sega, .color_king, .color_indigo, .color_lavender, .color_purple, .color_magenta, .color_pink.

Background colors: .color_bg_1 through .color_bg_12.

Layout classes: .width_1 through .width_6, .width_full, .width_half, .flex_1 through .flex_6, .padded.

Widgets

All input widgets return true when their value changes. Container widgets (BeginPanel, BeginFolder, Header) return true when expanded.

Ref

GUI.Ref(key, initialValue) creates a persistent local value stored in localStorage. It survives page reloads.

const myRef = GUI.Ref('key', 0);
myRef();       // read
myRef(42);     // write

Refs can be used as the target for any input widget:

GUI.Number(GUI.Ref('myNumber', 50), 'My Number', 0, 100);
GUI.Checkbox(GUI.Ref('debug', false), 'Debug mode');

They can also be passed as parameters to control the state of container widgets like BeginFolder, Header, BeginTabs, and ToggleButton:

if (GUI.BeginFolder('Settings', GUI.Ref('settingsOpen', true))) {
  // folder state persists across reloads
  GUI.EndFolder();
}
if (GUI.Header('Advanced', GUI.Ref('advancedOpen', false))) {
  // header state persists across reloads
}
GUI.ToggleButton('Dark mode', GUI.Ref('darkMode', false));
GUI.BeginTabs('view', GUI.Ref('selectedTab', 'Scene'));

Refs prefixed with @ are global (shared across all panels):

GUI.Number(GUI.Ref('@globalSpeed', 1), 'Speed', 0, 10);

Panel

if (GUI.BeginPanel('Title')) {
  // panel content
  GUI.EndPanel();
}

| Param | Type | Default | Description | |-------|------|---------|-------------| | title | string | 'GUI' | Panel title | | fitContent | boolean | true | true: height adjusts to content. false: fixed height, scrollable |

Returns false when the panel is hidden. Inside a GUI.render() callback, you can use an early return to avoid nesting:

GUI.render(() => {
  if (!GUI.BeginPanel('Title')) return;
  // panel content
  GUI.EndPanel();
});

Panels are draggable, resizable, and can be collapsed. Escape toggles all panels.

Window

Lower-level primitive used internally by BeginPanel. Use this when you need full control over the window chrome (custom title bars, no default collapse/close buttons).

const visible = GUI.Ref('@myWindowVisible', true);
const obj = { speed: 50 };

GUI.render(() => {
  const active = GUI.BeginWindow('My Window', true, visible);
  if (active) {
    GUI.WindowBar('My Window');
    GUI.Slider(obj, 'speed', 0, 100);
  }
  GUI.EndWindow();
});

| Param | Type | Default | Description | |-------|------|---------|-------------| | title | string | | Window title (used as widget identity) | | fitContent | boolean | true | true: height adjusts to content. false: fixed height, scrollable | | visible | boolean | Ref | undefined | Visibility state. Accepts a Ref to persist | | right | number | Ref | undefined | Right offset in pixels. Accepts a Ref to persist | | top | number | Ref | undefined | Top offset in pixels. Accepts a Ref to persist | | width | number | Ref | undefined | Width in pixels. Accepts a Ref to persist | | height | number | Ref | undefined | Height in pixels. Accepts a Ref to persist |

BeginWindow returns true when the window is active (visible and ready for content). Always call EndWindow() after BeginWindow, regardless of the return value.

WindowBar(title) renders a draggable title bar. Unlike BeginPanel, no collapse/close buttons are added automatically — you build the bar contents yourself.

Folder

if (GUI.BeginFolder('Section', true)) {
  // folder content
  GUI.EndFolder();
}

| Param | Type | Default | Description | |-------|------|---------|-------------| | label | string | | Folder label | | expanded | boolean | Ref | undefined | true: expanded. false: collapsed. Omit: always open (no header). Accepts a Ref to persist state |

FolderHeader

Custom folder headers let you place extra widgets (buttons, labels, etc.) inside the header row, next to the toggle:

const expanded = GUI.BeginFolderHeader('Physics', true);
  GUI.Button('Reset');       // appears in the header row
GUI.EndFolderHeader();
if (expanded) {
  GUI.Slider(obj, 'gravity');
  GUI.Slider(obj, 'drag');
  GUI.EndFolder();
}

| Param | Type | Default | Description | |-------|------|---------|-------------| | label | string | | Folder label | | expanded | boolean | Ref | undefined | true: expanded. false: collapsed. Accepts a Ref to persist state |

BeginFolderHeader returns true when expanded. Between BeginFolderHeader and EndFolderHeader, widgets are added to the header row. After EndFolderHeader, widgets go into the folder content (if expanded). Close with EndFolder like a regular folder.

Tabs

GUI.BeginTabs('myTabs');
  if (GUI.Tab('First')) {
    GUI.Text(obj, 'name');
  }
  if (GUI.Tab('Second')) {
    GUI.Number(obj, 'x', 0, 100);
  }
GUI.EndTabs();

| Param | Type | Default | Description | |-------|------|---------|-------------| | label | string | | Tab container id | | selected | string | Ref | '' | Default selected tab name. Accepts a Ref to persist state |

GUI.Tab(label) returns true when its tab is active.

Header

if (GUI.Header('Section', true)) {
  // collapsible content
}

| Param | Type | Default | Description | |-------|------|---------|-------------| | label | string | | Header label | | expanded | boolean | Ref | undefined | true: expanded. false: collapsed. Omit: static (non-collapsible). Accepts a Ref to persist state |

Group

GUI.BeginGroup('myRow', 'div.row');
  GUI.ButtonInput('A');
  GUI.ButtonInput('B');
GUI.EndGroup();

| Param | Type | Default | Description | |-------|------|---------|-------------| | id | string | | Group identifier | | elType | string | 'div.col' | 'div.row' for horizontal, 'div.col' for vertical |

SetParent

Redirect widgets to a named container:

GUI.SetParent('#col1');
if (GUI.BeginPanel('Panel in col1')) {
  GUI.EndPanel();
}

| Param | Type | Description | |-------|------|-------------| | id | string | A #widgetId, an element id, or 'body' |

Text

GUI.Text(obj, 'name');
GUI.Text(obj, 'name::"Display Name"');

Label + text input. TextInput is the standalone variant (no label).

Number

GUI.Number(obj, 'x');
GUI.Number(obj, 'x::"Position X"', -100, 100, 0.5);

Label + number input. Shows a mini slider when both min and max are finite. NumberInput is the standalone variant.

| Param | Type | Default | Description | |-------|------|---------|-------------| | min | number | -Infinity | Minimum value | | max | number | Infinity | Maximum value | | step | number | 0 | Step increment. 0 = free |

Slider

GUI.Slider(obj, 'volume', 0, 1, 0.01);

Label + range slider + number input. SliderInput is the standalone variant.

| Param | Type | Default | Description | |-------|------|---------|-------------| | min | number | -Infinity | Minimum value | | max | number | Infinity | Maximum value | | step | number | 0 | Step increment |

Vector

GUI.Vector(obj, 'position::"Pos"', -500, 500, 1);

Label + number inputs for each component. Works with arrays ([x, y, z]) and objects ({x, y, z}). VectorInput is the standalone variant.

| Param | Type | Default | Description | |-------|------|---------|-------------| | min | number | -Infinity | Minimum value per component | | max | number | Infinity | Maximum value per component | | step | number | 0 | Step increment |

Checkbox

GUI.Checkbox(obj, 'enabled');

Label + checkbox. CheckboxInput is the standalone variant.

Select

GUI.Select(obj, 'choice::"Pick one"', ['Apple', 'Banana', 'Cherry']);
GUI.Select(obj, 'mode', { a: 'Option A', b: 'Option B' });

Label + dropdown. SelectInput is the standalone variant.

| Param | Type | Description | |-------|------|-------------| | list | array | object | Array: options are values, selected value is the index. Object: keys are values, values are display labels |

Color

GUI.Color(obj, 'tint');

Label + color picker. ColorInput is the standalone variant (swatch only).

Supported value formats: hex strings ('#e74c3c', '#3498dbaa'), CSS strings ('rgb(46, 204, 113)', 'tomato'), integers (0x1abc9c), arrays ([0.9, 0.6, 0.1]), and objects ({r: 1, g: 0.2, b: 0.4}).

Button

if (GUI.Button('Click me')) {
  // clicked this frame
}
if (GUI.Button('<{play}>Play')) {
  // button with icon
}

Full-row button. Returns true on click. ButtonInput is the inline variant (use in rows).

Icon syntax: <{iconName}> before the label text.

ToggleButton

GUI.ToggleButton('Toggle', GUI.Ref('toggled', false));

Full-row toggle. ToggleButtonInput is the inline variant.

| Param | Type | Default | Description | |-------|------|---------|-------------| | label | string | | Button label | | toggled | boolean | Ref | undefined | Initial state. Accepts a Ref to persist state |

Inside a BeginToggleGroup, toggle buttons act as radio buttons:

GUI.BeginToggleGroup('align', 'left');
  GUI.ToggleButtonInput('Left');
  GUI.ToggleButtonInput('Center');
  GUI.ToggleButtonInput('Right');
GUI.EndToggleGroup();

Point2D

const obj = { pos: [0.5, 0.5] };
GUI.Point2D(obj, 'pos', 0, 1, 0, 1);

const obj2 = { pts: [0.2, 0.8, 0.5, 0.3, 0.8, 0.7] };
GUI.Point2D(obj2, 'pts::"Multiple"', 0, 1, 0, 1);

Label + 2D draggable area. Value is a flat array of [x, y] pairs. Point2DArea is the standalone variant. Point2DInput is an individual point handle inside a Point2DArea.

| Param | Type | Default | Description | |-------|------|---------|-------------| | minX | number | 0 | Minimum X value | | maxX | number | 1 | Maximum X value | | minY | number | 0 | Minimum Y value | | maxY | number | 1 | Maximum Y value | | step | number | 0 | Snap increment | | paddingX | number | 0 | Horizontal padding (fraction of range) | | paddingY | number | 0 | Vertical padding (fraction of range) | | height | number | undefined | Height in row units. Omit for auto (aspect ratio from ranges) |

Scrubber

GUI.Scrubber(obj, 'time', 0, 100, 0, 100, 1);
GUI.Scrubber(obj, 'time::"Clamped"', 10, 80, 0, 100, 1, true);

Label + scrubber slider + number input. ScrubberInput is the standalone variant.

| Param | Type | Default | Description | |-------|------|---------|-------------| | rangeStart | number | Ref | | Start of the visible range | | rangeEnd | number | Ref | | End of the visible range | | min | number | -Infinity | Minimum value | | max | number | Infinity | Maximum value | | step | number | 0 | Step increment | | clamped | boolean | Ref | undefined | When set, shows a clamp toggle and range handles |

PlotLine

// Data: flat array of [x0, y0, x1, y1, ...]
const data = { sine: new Array(120) };
for (let i = 0; i < 60; i++) {
  data.sine[i * 2] = i / 59;
  data.sine[i * 2 + 1] = Math.sin(i / 59 * Math.PI * 4);
}
GUI.PlotLine(data, 'sine', 0, 1, -1, 1, 4);

Label + canvas line chart. PlotLineCanvas is the standalone variant. Use NaN for x to create gaps between segments.

| Param | Type | Default | Description | |-------|------|---------|-------------| | minX | number | undefined | Minimum X bound. Omit for auto | | maxX | number | undefined | Maximum X bound. Omit for auto | | minY | number | undefined | Minimum Y bound. Omit for auto | | maxY | number | undefined | Maximum Y bound. Omit for auto | | height | number | undefined | Height in row units |

Monitor

GUI.Monitor(stats, 'fps::"FPS".color_green', 0, 100);

Real-time rolling value plot. Appends the current value each frame. MonitorCanvas is the standalone variant.

| Param | Type | Default | Description | |-------|------|---------|-------------| | min | number | 0 | Minimum value | | max | number | 100 | Maximum value | | step | number | 0 | Step increment | | storeArray | number[] | undefined | Optional shared data array | | height | number | 2 | Height in row units |

Space

GUI.Space();

Visual separator.

Label

GUI.Label('My label');
GUI.Label('<{play}>Play label');

Standalone label element.

Html

GUI.Html('p.padded', 'Simple paragraph');
GUI.Html('p.padded.color_sky', '<strong>Bold</strong> and <em>italic</em>');

Arbitrary HTML element.

| Param | Type | Description | |-------|------|-------------| | elType | string | Element tag with optional classes (e.g. 'div.padded.color_red') | | html | string | number | Inner HTML content |

IsKeyDown

Checks if a key is currently pressed. Use inside a render callback to react to keyboard input every frame.

GUI.render(() => {
  // Cmd/Ctrl+E to reset
  if (GUI.IsKeyDown('Meta') && GUI.IsKeyDown('KeyE', true)) {
    obj.speed = 1;
  }
});

| Param | Type | Default | Description | |-------|------|---------|-------------| | code | string | | Key code (e.g. 'KeyS', 'Escape'). Use 'Meta' for Cmd (Mac) or Ctrl (Windows/Linux) | | preventDefault | boolean | false | When true, blocks the browser default for this key when Cmd/Ctrl is held |

Returns true when the key is currently pressed.

The preventDefault flag registers the key so that future keydown events with Cmd/Ctrl held will have their browser default suppressed. This only affects key presses combined with Cmd/Ctrl — plain key presses (e.g. typing 'S' in a text input) are never blocked.

Gotchas

A few things to keep in mind when working in immediate mode.

Avoid allocations in render callbacks. Your render function runs every frame. Don't create objects, arrays, or concatenate strings inside it. Declare data outside and mutate it.

// Bad: allocates a new object and a new array every frame
GUI.render(() => {
  GUI.Vector({ pos: [x, y] }, 'pos');
});

// Good: reuse the same object
const obj = { pos: [0, 0] };
GUI.render(() => {
  GUI.Vector(obj, 'pos');
});

Avoid DOM queries in render callbacks. Methods like querySelector, closest, getBoundingClientRect, offsetHeight, etc. are expensive when called every frame. Cache references outside the render loop, or gate them behind a condition so they only run once.

Don't change object shapes. Don't add new properties to objects after creation. This causes V8 hidden class transitions and deoptimizations. Declare all properties upfront. If you need to add a value dynamically, use GUI.Ref instead.

// Bad: adding properties later
const obj = {};
obj.speed = 1;

// Good: declare everything at creation
const obj = { speed: 1 };

// Also good: use a Ref for dynamic/ad-hoc values
GUI.Number(GUI.Ref('speed', 1), 'Speed', 0, 10);

Conditional widgets are fine. Unlike retained mode GUIs, you can freely show or hide widgets with regular if statements. Widgets that stop being declared are automatically removed from the DOM.

GUI.render(() => {
  GUI.Checkbox(obj, 'advanced');
  if (obj.advanced) {
    GUI.Number(obj, 'detail', 0, 100);
  }
});

Always check the return value of Begin* and Header widgets. BeginPanel returns false when the panel is hidden. BeginFolder, BeginFolderHeader, and Header return false when collapsed. When the return value is false, skip the content and the matching End* call.

GUI.render(() => {
  if (GUI.BeginPanel('Settings')) {
    if (GUI.BeginFolder('Advanced', false)) {
      GUI.Number(obj, 'detail', 0, 100);
      GUI.EndFolder();
    }
    const expanded = GUI.BeginFolderHeader('Custom', true);
      GUI.Button('Reset');
    GUI.EndFolderHeader();
    if (expanded) {
      GUI.Number(obj, 'detail', 0, 100);
      GUI.EndFolder();
    }
    if (GUI.Header('Debug', false)) {
      GUI.Checkbox(obj, 'wireframe');
    }
    GUI.EndPanel();
  }
});

You can call GUI.render() as many times as you want. Each call registers a separate callback that runs every frame. Widgets are appended to a default panel if none is declared, or you can target a specific panel with SetParent.

// Each render callback can declare its own panel
GUI.render(() => {
  if (GUI.BeginPanel('Panel A')) { /* ... */ GUI.EndPanel(); }
});

// Or add widgets to an existing panel from another callback
GUI.render(() => {
  GUI.SetParent('#panelA');
  GUI.Number(obj, 'x', 0, 100);
});