Saving and loading
Values autosave to localStorage, keyed by location.pathname; use createGUI({ persist: 'id' }) to pick your own key. Only values you changed are stored, together with the code default they were made against. So:
- Adding controls in code never fights saved state.
- If you later change a default in code, the row shows a hollow dot ("code default changed"). Right-click it to take the new default or keep your value.
The layout (open folders, windows, pins, the panel's place) is saved too. To choose what's saved:
persist: falseon a control or a folder leaves it out (a folder's covers every control under it, unless one says otherwise);persist: trueputs it back in. It's left out of presets and value files too (run state, such as a paused toggle), but Copy as code still covers it: a copy has every value the panel shows.createGUI({ persist: { values: false } })saves no values except those a folder or control opts into, and keeps the layout. A value that isn't saved is still marked modified while you work (a double-click resets it), and what you export or load yourself (the menu's files,save(),load(), Copy all as code, presets) covers it.{ layout: false }saves values but not the layout: open folders, windows, pins, the panel's place and whether it's hidden, the texture viewer's settings. A file linked from the panel is still remembered.{ id }sets the key.
An app that keeps its state in its URL, or whose controls start loads when set, can keep the layout and save only its look:
const gui = createGUI({ persist: { values: false } });
gui.add(state, 'scene', { options: scenes }); // the URL decides; never restored
gui.folder('Look', { persist: true }).add(look, 'grain'); // saved and restored
Menu options:
- Save values to file… and Load values from file… (or drop a
.jsononto the panel). - Copy all as code, which gives a pasteable object literal of every value, for baking tweaks into your defaults. Copy all changes as code gives only the values you changed. A folder's menu has both for that folder.
- Reset all and Forget saved values.
From code: gui.save() and gui.load(doc), gui.download() and gui.upload(), and gui.toCode().
Keep values in your project
import tweaks from './tweaks.json';
gui.linkFile('src/tweaks.json', { initial: tweaks });
The path is relative to the project root (where the dev server runs), not to the module. To start, create the file empty: {} (or { "devui": 1, "values": {} }, the format devui writes). You can also call linkFile('src/tweaks.json') without initial first: the dev server creates the file on the first change, and then you add the import. In the file, each control's entry has v (its value as JSON), d (the code default it was tuned against, to spot when the code changes) and n (the value as a plain literal, for the noop build).
The file is a layer between code defaults and your local tweaks, so values you commit reach everyone, and local storage only keeps what differs from the file. Presets are layered the same way: a preset you changed, added or deleted is kept locally until the file has it, and the others follow the file, so a teammate's change to one reaches you rather than being written over. While the file is writable, every finished change (a whole drag, an edit, a preset) is written to it, formatted one control per line so diffs read well. It's writable when:
- Vite: add the plugin. It also stops devui's own writes from triggering HMR:
ts import { devuiFiles } from '@haxiomic/devui/vite'; export default { plugins: [devuiFiles()] }; - esbuild or any other server: run the sidecar and pass its URL. Pages on localhost may use it; add
--host 0.0.0.0 --origin http://<ip>:<port>for another device such as an iPad.sh npx devui-files . # writes .json files under this folder, port 5179ts gui.linkFile('src/tweaks.json', { initial: tweaks, server: 'http://localhost:5179/__devui' }); - Chromium, no server: click Link… in the panel (or Link values file…) and pick the file. The browser remembers it; after a restart it asks once to allow writing. This works without any
linkFilecall in code too.
linkFile also takes url (where to read the file when there's no dev server: a copy served next to the page) and server: false (never write through one). gui.file?.status is how the link is (linked, read-only, needs-permission…; the file event says when it changes), and gui.unlinkFile() lets it go.
Otherwise the file is read-only and tweaks stay in the browser. Production builds apply it: the noop build applies initial (and gui.load(doc)) to params() and add(obj, key) bindings, so a shipped build uses the values you tuned. Controls created by inspectors (gui.add(material)) need the values copied into code (Copy all as code).