← All reusables

tweakpane

integration

Installation

npx jsrepo add github/reatom/reusables tweakpane

Registry dependencies

Documentation

Tweakpane Integration

Reactive bindings for Tweakpane — a compact GUI for fine-tuning values and monitoring data. This integration manages Tweakpane's lifecycle automatically through Reatom's subscription model.

Installation

This integration requires tweakpane and @tweakpane/plugin-essentials:

npm install tweakpane @tweakpane/plugin-essentials

Core Concepts

All Tweakpane resources (panes, folders, bindings) are wrapped in computed atoms that:

  • Create the resource only when subscribed to
  • Automatically dispose when unsubscribed
  • Support reactive updates

API Reference

reatomPane(params)

Creates a reactive Tweakpane instance.

import { reatomPane } from '#reatom/tweakpane'

const pane = reatomPane({ name: 'settings', title: 'Settings' })

// Activate by subscribing
pane.subscribe()

reatomPaneFolder(params, parent)

Creates a collapsible folder inside a pane.

const folder = reatomPaneFolder({ title: 'Transform' }, pane)

reatomPaneTab(pages, parent)

Creates a tabbed interface.

const tabs = reatomPaneTab(['General', 'Advanced'], pane)
// Access pages
const generalPage = tabs.pages[0]

withBinding(params, parent)

Extension that creates a two-way binding between an atom and a Tweakpane control.

import { atom } from '@reatom/core'
import { withBinding } from '#reatom/tweakpane'

const speed = atom(1.0, 'speed').extend(
  withBinding({ label: 'Speed', min: 0, max: 10 }, pane),
)

// Subscribe to activate the binding
speed.subscribe((v) => console.log('Speed:', v))

Automatic view detection: Tweakpane automatically chooses the appropriate control based on the initial value:

Value Type Control
number Slider/Number
boolean Checkbox
string Text input
#rrggbb Color picker
{ x, y } / {x,y,z} Point editor
reatomEnum Dropdown

withButton(params, parent)

Extension that triggers an action from a button.

import { action, effect, getCalls } from '@reatom/core'
import { withButton } from '#reatom/tweakpane'

const reset = action(() => {
  // reset logic
}, 'reset').extend(withButton({ title: 'Reset' }, pane))

// Must subscribe to activate
effect(() => getCalls(reset))

reatomFpsGraph(params, parent)

Creates an FPS monitoring graph.

import { reatomFpsGraph } from '#reatom/tweakpane'

const fps = reatomFpsGraph({ label: 'FPS' }, pane)
fps.subscribe()

function render() {
  fps().begin()
  // ... render ...
  fps().end()
  requestAnimationFrame(render)
}

Complete Example

import { action, atom, effect, reatomEnum, getCalls } from '@reatom/core'
import {
  reatomPane,
  reatomPaneFolder,
  withBinding,
  withButton,
} from '#reatom/tweakpane'

// Create pane
const pane = reatomPane({ name: 'game', title: 'Game Settings' })

// Basic values
const speed = atom(1.0, 'speed').extend(
  withBinding({ label: 'Speed', min: 0, max: 10 }, pane),
)

const color = atom('#ff0000', 'color').extend(
  withBinding({ label: 'Color' }, pane),
)

const difficulty = reatomEnum(['easy', 'medium', 'hard'], 'difficulty').extend(
  withBinding({ label: 'Difficulty' }, pane),
)

// Folder for transforms
const transform = reatomPaneFolder({ title: 'Transform' }, pane)

const position = atom({ x: 0, y: 0 }, 'position').extend(
  withBinding({ label: 'Position' }, transform),
)

const scale = atom(1.0, 'scale').extend(
  withBinding({ label: 'Scale', min: 0.1, max: 5 }, transform),
)

// Reset button
const reset = action(() => {
  speed.set(1.0)
  position.set({ x: 0, y: 0 })
  scale.set(1.0)
}, 'reset').extend(withButton({ title: 'Reset All' }, pane))

// Activate everything
speed.subscribe()
color.subscribe()
difficulty.subscribe()
position.subscribe()
scale.subscribe()
effect(() => getCalls(reset))

Lifecycle Management

Resources are automatically disposed when their atoms are unsubscribed:

const pane = reatomPane({ name: 'temp' })
const unsub = pane.subscribe()

// Later: disposes the pane and all its children
unsub()

This integrates seamlessly with Reatom's effect for component-scoped GUIs.

Example

import { action, atom, reatomEnum } from '@reatom/core'
import { reatomComponent } from '@reatom/react'

import { withBinding } from './bindings'
import { withButton } from './blades'
import { reatomPane, reatomPaneFolder, reatomPaneTab } from './core'
import { hotWrap } from '../hot-wrap/hot-wrap'

// --- Tweakpane with reatomComponent ---
// Atoms are auto-subscribed when read in render
// Actions use hotWrap for subscription + event handler wrapping

const settingsPane = reatomPane({ name: 'settings', title: 'Settings' })

const speed = atom(1.0, 'speed').extend(
  withBinding({ label: 'Speed', min: 0, max: 10, step: 0.1 }, settingsPane),
)

const enabled = atom(true, 'enabled').extend(
  withBinding({ label: 'Enabled' }, settingsPane),
)

const position = atom({ x: 0, y: 0 }, 'position').extend(
  withBinding({ label: 'Position' }, settingsPane),
)

const mode = reatomEnum(['idle', 'running', 'paused'], 'mode').extend(
  withBinding({ label: 'Mode' }, settingsPane),
)

const resetAction = action(() => {
  speed.set(1.0)
  enabled.set(true)
  position.set({ x: 0, y: 0 })
  mode.reset()
}, 'reset').extend(withButton({ title: 'Reset All' }, settingsPane))

export const SettingsPanel = reatomComponent(() => {
  // Reading atoms auto-subscribes them → Tweakpane bindings activate
  const currentSpeed = speed()
  const isEnabled = enabled()
  const pos = position()
  const currentMode = mode()

  return (
    <div>
      <p>Speed: {currentSpeed} </p>
      <p> Enabled: {isEnabled ? 'Yes' : 'No'} </p>
      <p>
        Position: {pos.x}, {pos.y}
      </p>
      <p>Mode: {currentMode}</p>
      {/* hotWrap for actions: subscribes (creates button) + wraps for onClick */}
      <button onClick={hotWrap(resetAction)}> Reset </button>
    </div>
  )
}, 'SettingsPanel')

// --- Folders example ---

const transformFolder = reatomPaneFolder({ title: 'Transform' }, settingsPane)

const scale = atom(1.0, 'scale').extend(
  withBinding({ label: 'Scale', min: 0.1, max: 5 }, transformFolder),
)

const rotation = atom(0, 'rotation').extend(
  withBinding({ label: 'Rotation', min: 0, max: 360 }, transformFolder),
)

export const TransformPanel = reatomComponent(() => {
  return (
    <div>
      <p>Scale: {scale()} </p>
      <p> Rotation: {rotation()}°</p>
    </div>
  )
}, 'TransformPanel')

// --- Tabs example ---

const tabsPane = reatomPane({ name: 'tabs-demo', title: 'Demo' })
const tabs = reatomPaneTab(['General', 'Advanced', 'Debug'], tabsPane)

const volume = atom(0.8, 'volume').extend(
  withBinding({ label: 'Volume', min: 0, max: 1 }, tabs.pages[0]),
)

const debugMode = atom(false, 'debugMode').extend(
  withBinding({ label: 'Debug' }, tabs.pages[2]),
)

export const TabsPanel = reatomComponent(() => {
  return (
    <div>
      <p>Volume: {Math.round(volume() * 100)}% </p>
      <p> Debug: {debugMode() ? 'On' : 'Off'} </p>
    </div>
  )
}, 'TabsPanel')