Documentation

Table of Contents

1. Introduction

Actually Integrated is a browser-based builder for HMI-style pages and dashboards: drag widgets onto a canvas, wire them to live data, and export the result as a single self-contained page. There's no account, no install, and no server — everything you build lives in your browser until you export it.

This page describes every part of the builder in detail. Use the contents on the left (or the list below on mobile) to jump to what you need, or read it straight through if you're starting from scratch.

New here? The ? icon inside the builder (top bar, next to AI Context) opens a short interactive walkthrough that covers the same basics hands-on, in the app itself — this page is the deeper reference to come back to afterward. Prefer to see it built first? The Examples page walks through building and exporting a real dashboard, with real screenshots at every step. Want the builder as its own installed app — its own window and icon, and working offline once you've opened it online at least once? See the App page for install steps on Chrome, Edge, and Safari.

2. The Builder Tab

Where you place and arrange widgets on the canvas. This is the tab you land on when you open the builder.

Canvas basics

  • Add a widget by clicking a template in the left Templates panel — templates are click-to-add, not drag-and-drop.
  • Move a widget by dragging it; resize using the corner and bottom/right edge handles (there are no top/left edge handles — reach those sides via a corner).
  • Select a widget by clicking it. Clicking additional widgets adds them to the selection (selection is additive, not toggled); clicking empty canvas clears it. The first widget you select is the "primary" one (its handles are a different color) and anchors the align/distribute toolbar that appears once you have 2 or more selected.
  • Delete a widget by dragging it fully off the canvas edge — you'll be asked to confirm before it's removed. Canceling snaps it back to where the drag started.
  • Grid snapping (toggle with Ctrl+G) is a builder-only alignment aid, from 1–200px. It has no effect on your exported page's coordinates.
  • Undo/redo with Ctrl+Z covers widget moves and resizes on the current page.
The working canvas has a comfortable minimum size while you're editing, even if your configured export size is smaller — but the page you actually export always uses the exact width/height you set in the background config, with no minimum applied.

Template library

The left panel's Library tab has three sub-tabs:

  • JavaScript — code-driven widgets: text, shapes, buttons, counters, charts, forms, clocks, a live variable inspector, and more. These run directly on the page and can read and write global variables.
  • HTML — markup-driven widgets (a landing-page block, a font manager, a dashboard shell, an embeddable web page viewer). These render in an isolated preview while you're building, and can read variables for things like theming.
  • Local — widgets you've previously saved yourself with a widget's own "Save as Template" button (see below), loaded back in from a .json file.

Use the search box at the top of the Library panel to filter the catalog by name as it grows — new templates are added over time, so this list is not meant to be exhaustive here.

Global colors & fonts (Styles tab)

Next to Library is a Styles tab for two things that are shared across your whole project:

  • Global colors — reusable color values that widgets can bind to instead of a hardcoded hex value, so changing one color here updates every widget using it. When you select one or more widgets on the canvas, this panel's add-widget buttons are replaced with Colour, Text, Font, Events, and Tags tools that act on the selected widget(s) — the first three scan for hardcoded values of that kind, Events composes and inserts trigger/action code, and Tags finds and rebinds which Data Connection variable a widget actually reads or writes (see below for all five). The same five tools are also always available with nothing selected, from Tools > Style Tools in the menu bar (acting on every widget on the current page), and from the Editor tab's own toolbar (acting on whichever widget you have open there).
  • Fonts — upload a font file and it becomes available to every widget on every page, immediately, without re-uploading it per widget.
If you combine multiple templates that each use their own numbered color slots, hovering the "ⓘ" next to a color in this panel shows you which templates read that slot and what they use it for — handy for spotting an accidental collision before it surprises you.

Layers & window properties (Tools panel)

The right "Tools" panel covers everything about an individual widget or the page as a whole:

  • Layers — every widget sits on a numbered layer (higher = drawn on top). Toggle a layer's visibility, rename it, add or remove layers, and set which layer new widgets land on. Hiding a layer doesn't just hide it visually — a JavaScript widget on a hidden layer stops running entirely (its timers pause) until the layer is shown again. Click a widget's name in the Layers list to select it (Shift/Ctrl-click adds more) — handy for widgets with nothing to click on the canvas, like the zero-size Alarm Tracker; double-click the name to rename it. Deleting a layer moves its widgets to layer 1 rather than deleting them; you'll be warned first if that layer isn't empty.
  • Properties — with a widget selected, this is the main place to edit it: its Name and Description, numeric Position (X/Y) and Size (Width/Height), Rotation, a row of action buttons (move it up/down a layer, Save as Template, jump to the Editor tab, or Delete it), and — for JavaScript and HTML widgets — a full code editor at the bottom of the panel, described below.
  • Background & app name — with nothing selected, this panel shows your Page Name, App Name (used as the browser tab title and the exported file name), and Background Settings (canvas width/height/color) — plus a project-wide Page JS editor for helper code shared by every widget (see below).
Editing a widget's code from the Properties panel. Select a JavaScript or HTML widget, open the Tools → Properties tab, and scroll past its position/size fields — the bottom of the panel is a real code editor bound directly to that widget's code (jsCode for JavaScript, content for HTML), with syntax highlighting and a drag handle above it to resize how much vertical space it takes up. It's the same underlying data as the code editor that appears on the canvas when you click a widget's own Edit button, and the same data the standalone Editor tab opens — edit in any of the three places and the others stay in sync automatically. For a JavaScript widget, changing the code here does not re-run it by itself; click that widget's own ▶ Run/Edit control (on the widget, or via Open in Editor) to apply it. HTML widgets update their preview live as you type, no separate run step needed. When nothing is selected, this same editor space is repurposed for Page JS — helper code that applies to the whole project (for example window.myHelper = () => { /* ... */ };) which any widget's own code can then call; it has its own ▶ Run button to re-apply it against the live canvas on demand.

Pages

The page tab strip above the canvas lets you build a multi-page project. Each page has its own widgets, layers, and background — but global variables and data connections are shared across every page.

  • Click + to add a page; double-click a tab to rename it; click its × to delete it (hidden once only one page remains).
  • Drag a tab to reorder it — the pin icon on each tab marks and lets you set the default/landing page: whichever page sits first is the one your exported site opens on. Click the greyed pin on any other tab to make that page the new default.

Widget Types

Every widget on the canvas is one of three types, and the type decides how it runs.

JavaScript widgets

These run directly on the page (not in an iframe) inside a sandboxed function, so your code sees its own scoped document/window that only reaches the widget's own container by default — while still having real access to things like fetch, setTimeout, and global variables.

  • A small floating toolbar appears on the widget itself while it's hovered or selected: a drag grip (⠿) to move it, and either a ▶ Run button (before it's been run) or an Edit button (once it has) — Edit clears the last run's output and reopens the code for changes.
  • Editing the code does not re-run it automatically — click ▶ Run again to apply changes (it also runs once automatically right after you add it).
  • For a bigger editing surface, or to change position/size/name alongside the code, use the Properties panel's own code editor instead — see Editing a widget's code from the Properties panel above. Both edit the exact same code, kept in sync.
  • This is the widget type that can read and write global variables, call page-navigation functions, and use every function described in the Runtime Reference below.

HTML widgets

These render your raw HTML/CSS/JS markup. While you're building, it's shown in a sandboxed preview frame that live-updates as you type — no separate "Run" step needed.

  • The widget's own floating toolbar has a single Edit / Preview toggle button to switch between the live preview and the raw markup.
  • HTML widgets can read global variables (for example to theme a color from a value you've set elsewhere) but they only see a snapshot taken when the preview last rebuilt — they don't write values back.
  • On export, this isolation goes away: your HTML is placed directly into the final page, at the same trust level as the page itself. Keep that in mind if you're pasting in markup from somewhere you don't fully trust.

Image widgets

A simple image drop-zone — drag an image onto it or click to browse, and it displays that image. No code involved.

Save as Template

Any widget — of any type — can be saved to your own computer as a .json template file, using the Save as Template button in the Properties panel's action row (select the widget, then Tools → Properties). Load it back in later from the Library's Local tab, on this project or any other.

Buttons & Controls Reference

Every button shown as it actually looks in the app, organized by where you find it — not just named, so you know exactly what to look for.

Canvas toolbar

The bar directly above the canvas changes with your selection:

ButtonPurpose
JavaScript HTML Media
Adds a new blank widget of that type. Shown only when nothing is selected.
Colour Text AaFont Events Tags
Replaces the three add-widget buttons the moment 1+ widgets are selected (also always reachable from Tools > Style Tools and the Editor tab — see above). Full walkthrough for each — what Colour/Text/Font/Tags scan for and the code they write, what Events composes and inserts — just below.
20 px ▾ Toggles the alignment grid and sets its spacing (1–200px), with quick presets in the small dropdown arrow. Builder-only — has no effect on your exported page.
When off, an idle widget's outline only appears on hover/selection instead of always — a decluttering aid while you work.
Window name Edit Shown when exactly one widget is selected — the field renames it inline (the same name used everywhere else: Layers list, Properties, the runtime's window.windows['Name']); Edit jumps straight to that widget in the full Editor tab.
Appear once 2+ widgets are selected, in this order: align left/right/top/bottom/center (horizontal or vertical), match width/height/both to the first-selected widget, distribute with equal gaps (needs 3+ selected). The same actions are also in the Properties panel and, on mobile, the Tools menu.

Colour tool, in detail

Select one or more widgets and it replaces the add-widget buttons (also always reachable from Tools > Style Tools and the Editor tab, with nothing selected). It scans the selected widget(s)' JavaScript code for hardcoded colour values and lets you turn any of them into a real, editable global variable.

What it scans for — any top-level const/let/var whose name contains COLOR/COLOUR, or whose value looks like a hex (#1a1a1a) or rgb()/rgba() colour, regardless of name — including inside an array or object (RAMP = ['#111', '#222'] scans as two separate rows). A value already linked to a global variable is recognized and shown as already-tagged rather than offered again as hardcoded.

Colour tool — 2 widgets scanned
Digital ClockTEXT_COLOR
#1a1a1a
Digital ClockBG_COLOR
#0090ff
Text LabelTEXT_COLOR
#333333

Each row's Style Tag box is one of: — None (hardcoded) —, an existing global_style_* tag (listed with its current value), or + New Tag. Editing the value field on a row left at None just rewrites that literal in place:

// before
const TEXT_COLOR = '#1a1a1a';
// after typing a new value into the row's Value field
const TEXT_COLOR = '#ff0000';

+ New Tag mints the next free slot (e.g. global_style_4), seeds it with the row's current value, and rewrites the constant to read it — picking an existing tag does the same rewrite bound to that key instead:

const TEXT_COLOR = (window.globalVariables?.global_style_4) || '#1a1a1a';

Switching a tagged row back to — None — unlinks it (the tag itself is not deleted — other widgets may still be reading it):

const TEXT_COLOR = '#1a1a1a';

Text tool, in detail

Same mechanism as the Colour tool, for hardcoded display text instead. What it scans for — any top-level const/let/var holding a plain quoted string that the Colour and Font tools' own rules don't already claim (a name containing COLOR/FONT, or a hex/rgb-shaped value, goes to those tools instead even if it's technically a string).

Text tool — 1 widget scanned
Text LabelTEXT
Hello World

Same Style Tag box (global_text_* tags this time), same rewrite shape:

const TEXT = (window.globalVariables?.global_text_1) || 'Hello World';

Font tool, in detail

Same mechanism again, for font-related constants. What it scans for — any top-level constant whose name contains FONT or ALIGN (family, size, weight, text alignment) — name-based rather than value-based, since a font weight's own value ('700') looks identical to a plain string otherwise.

Aa Font tool — 1 widget scanned
Text LabelFONT_FAMILY
Arial, sans-serif
Text LabelFONT_SIZE
24

Same Style Tag box (global_font_* tags), same rewrite shape. A FONT_FAMILY row's dropdown also offers every font uploaded in Styles > Fonts directly, in addition to the plain Style Tag options.

const FONT_FAMILY = (window.globalVariables?.global_font_1) || 'Arial, sans-serif';

Events tool, in detail

A different shape of tool from Colour/Text/Font above: those three scan a widget's existing code for hardcoded values. Events doesn't scan anything — it composes a small, ready-to-run snippet from a trigger (when it should run) and an action (what it should do), shows you the exact code before anything happens, and appends it to a widget's code only once you click Insert.

Triggers — when the snippet runs:

  • Value Change — runs whenever a global variable's value changes, checked on a short poll (the same way every widget already reads globalVariables).
  • Timer — runs repeatedly on a fixed interval, for as long as the widget exists.
  • Hover — runs once each time the mouse enters a given element's area (a CSS selector, same as the actions below).
  • Click — runs each time a given element is clicked.

Actions — what the snippet does:

  • Change a value — sets a global variable.
  • Change text — sets an element's textContent, by CSS selector.
  • Change colour — sets an element's color or background-color, by CSS selector.
  • Change border — sets an element's border, by CSS selector.
  • Change any attribute — sets any attribute on an element, by CSS selector.

Every selector-based action reaches into the widget's own markup with document.querySelector(...) — never a bare container identifier. Some built-in widgets (Cloud Nav 3D among them) wrap their own code in a private (function () { ... })(); for local scoping, which puts anything they declare with const/let — including a variable some of them happen to call container — out of reach of code appended after that wrapper closes. document has no such problem: it's a sandbox-provided parameter of the widget's whole code, not something the widget's own code declares, so it's always reachable regardless of what the existing code does with its own locals — and it's already scoped to just this widget's real container, same as a hand-named container variable would have been.

Events tool — Value Change → Change a value
Global variable to watchglobal_style_1
Variable namemy_variable → New value'#ff0000'

The preview updates live as you fill in each field, so you always see the exact code before it's inserted — wrapped in a comment "fence" that carries the trigger's own id:

// >>> Events Tool: valueChange:global_style_1 >>>
(function () {
  let _last = JSON.stringify(globalVariables['global_style_1']);
  setInterval(function () {
    const cur = JSON.stringify(globalVariables['global_style_1']);
    if (cur !== _last) {
      _last = cur;
      globalVariables['my_variable'] = '#ff0000';
    }
  }, 300);
})();
// <<< Events Tool: valueChange:global_style_1 <<<

Clicking Insert into <widget name> appends exactly that fenced block to the end of the widget's JavaScript. Nothing runs until you click Insert; Copy is there if you'd rather paste it in by hand at a specific spot instead.

Re-inserting replaces, it doesn't stack. If this window already has a fenced block for the same trigger — the same kind for Timer/Hover/Click, or the same watched variable for Value Change — the dialog shows it under "Already on <widget name>" (with its own Remove button), and Insert becomes Replace on <widget name>: it finds that exact fenced block by its id and swaps it in place, selector/interval/action and all, instead of leaving the old one running underneath a second copy.

A Click trigger (selector .my-label) paired with a border change, for example, comes out as:

// >>> Events Tool: click >>>
document.querySelector('.my-label').addEventListener('click', function () {
  document.querySelector('.my-label').style.border = '2px solid #ff0000';
});
// <<< Events Tool: click <<<

Tags tool, in detail

A different job from Colour/Text/Font above, not a fourth bucket of the same scan: those three turn a hardcoded style value into a shared variable. Tags finds the field that names which Data Connection variable a widget reads or writes in the first place — the VARIABLE_NAME constant most data-bound widgets customize, and the SERIES list a few chart widgets use to plot more than one variable at once.

What it scans for — a top-level const/let/var named exactly VARIABLE_NAME, and every { name: '...', label: '...' } entry inside a SERIES array (each one its own row, labeled with its own label).

Tags tool — 1 widget scanned
Radial GaugeVARIABLE_NAME
loadPercent ● wired

No Style Tag box here — picking a value from the dropdown (every variable that exists in the project: everything in the Variables tab, every variable a Data Connection extracts, and any key another widget already uses — not just keys some widget's code names; the shared style/font/text colour slots are left out) rewrites the field directly to that key, rather than creating a new shared slot:

var VARIABLE_NAME = 'loadPercent';

The ● wired / ○ unwired marker next to each row (and in front of every dropdown option) shows whether that key currently has a real value in Data Connections — a quick way to spot a widget still showing its own demo/placeholder data because nothing has wired it up yet. You can also type a key directly into the field instead of picking one, the same as any other scan tool's value field.

On the widget itself

A small floating toolbar appears directly on a widget while it's hovered or selected:

ButtonPurpose
⠿Drag to move the widget.
▶ Run (JavaScript, before first run)Executes the widget's code.
Edit (JavaScript, after it's run)Clears the current output and reopens the code for changes — run it again with ▶ Run when you're ready.
Edit / Preview (HTML, one toggle button)Switches between the live rendered preview and the raw markup.

Properties panel (Tools → Properties, one widget selected)

ButtonPurpose
▲ Layer ▼ LayerMoves the widget one layer up or down. Disabled at the top/bottom layer.
Save as TemplateDownloads this widget as a reusable .json file (see §2 above).
Open in EditorJumps to this widget in the standalone Editor tab (JavaScript/HTML only).
DeleteRemoves the widget, after a confirmation prompt.
Code editor drag handleThe thin bar above the code editor at the bottom of the panel — drag it to resize how much vertical space the editor takes up.

See Editing a widget's code from the Properties panel above for how the code editor itself works.

Layers panel (Tools → Layers)

ButtonPurpose
+ Add New LayerCreates a new, empty layer above the current highest one.
▲ ▼Move layer up / down — reorders a layer's drawing position relative to the others.
✓ ✕Click a layer's name to rename it; ✓ saves, ✕ cancels.
Shows/hides every widget on that layer — hiding also pauses any JavaScript widgets on it.
Set ActiveChooses which layer a newly-added widget lands on.
✕Remove Layer — deletes it; if it has widgets on it, you're warned first, and they move to layer 1 rather than being deleted.
▲ ▼Move window up / down within a layer's own list — reorders widgets within the same layer.

File / Edit / View menus

ButtonPurpose
SaveFile → Save — downloads the re-importable JSON project save file (see §10 below).
Export ▾
File → Export — downloads your project as a self-contained HTML page or a JS bundle. Strip Comments: smaller output, useful for microcontroller flash/storage. Gzip: compresses the file further, with a Format choice between a pasteable hex byte-array for firmware source or a raw .gz file for a filesystem/web server.
PreviewFile → Preview — opens the export result in a new tab without downloading anything.
UndoCtrl+Z RedoCtrl+YEdit → Undo / Redo — covers widget moves and resizes on the current page.
DeleteDel CutCtrl+X CopyCtrl+C PasteCtrl+VEdit menu — standard selection editing.
− Page Zoom +View → Page Zoom / Canvas Zoom — the exported page's own zoom vs. the builder's canvas zoom, independent of each other.
Fit to ScreenView → Fit to Screen — zooms the canvas so the whole page background fits on screen without scrolling.
View menu — same toggles as the canvas toolbar, reachable here too.

Page tabs

ButtonPurpose
+Adds a new page.
×Deletes a page (hidden once only one page remains).
Double-click a tabRenames that page.
Drag a tabReorders pages.
Pin icon — solid on the current default/landing page; click a greyed pin on any other tab to make that page the new default.

Data Connections

ButtonPurpose
Test APIRuns the connection immediately and shows the raw response, with detailed troubleshooting if it fails.
Auto-populate VariablesGenerates a variable list automatically from the shape of the last test response.
Import from GatewayMerges connections and variables in from another exported project's JSON file.
Quick Start ▾Pre-fills a whole connection (generic GET/POST/PUT) as a starting point.
✕Delete (header / query string / parameter rows) — removes that one row from the connection's config.

Runtime Reference

Functions and values available inside a JavaScript widget's own code. This same reference is also built into the app itself — open Runtime Reference from the Builder tab for a live, copyable, searchable version scoped to your actual project (with the real names of your pages and widgets filled in).

Global scope — callable from any widget, on any page

CallWhat it does
goTo<PageName>()Navigate to a specific page by name — one function is generated per page in your project (for example a page named "Settings" gets window.goToSettings()).
getContainerSize()This widget's own real, live size — accurate in an exported page even if it's been resized responsively.
window.windows['Name'].rerun()Re-run a specific widget, by its display name, from anywhere — including from a different widget or a different page's code.
window.myPage.width / .height / .colorRead or set the current page's own background size/colour — the same values shown in Tools → Properties when nothing is selected.

Example — bounce this widget around the page, using its own live size so it never clips past an edge:

// Runs once, then re-positions this widget on every tick.
let vx = 2, vy = 2;

setInterval(() => {
    const size = getContainerSize();     // this widget's own real width/height
    const pageW = window.myPage.width;   // the page's own configured width
    const pageH = window.myPage.height;

    let x = window.myWindow.x + vx;
    let y = window.myWindow.y + vy;

    // Bounce off whichever edge this widget's own size would otherwise clip past.
    if (x <= 0 || x + size.width >= pageW) vx *= -1;
    if (y <= 0 || y + size.height >= pageH) vy *= -1;

    window.myWindow.x = x;
    window.myWindow.y = y;
}, 16);

Swap window.myWindow.x/.y for window.windows['Other Widget'].x/.y to drive a different widget's position instead — for example keeping a label glued just to the right of a gauge, using the gauge's own getContainerSize() to know how far right "just past it" actually is.

Window scope — only inside a widget's own code

Every one of these can also be written as window.windows['ThisWidget'].x (etc.) from anywhere else — but from inside a widget's own code, use window.myWindow instead of typing out its own name. myWindow always refers to whichever widget the currently-running code belongs to — it's a property of window, not a variable of its own, so it's always written window.myWindow, never bare myWindow.

CallWhat it does
window.myWindow.rerun()Re-run this widget from scratch.
window.myWindow.x / .yRead or set this widget's own position.
window.myWindow.width / .heightRead or set this widget's own size.
window.myWindow.layerRead or set this widget's own layer.
rerenderOn('varName')Re-run this widget automatically whenever the named global variable changes, instead of polling for it by hand.

Reading & writing global variables

Inside a JavaScript widget, a global variable named token is reachable three interchangeable ways:

globalVariables.token   // via the parameter passed into your widget
window.token             // as a bare property on window
token                     // as a bare identifier, if the name is valid JS

Writing to any of these updates the value everywhere else immediately — every other widget reading that same variable sees the change without needing a page reload.

View preferences

Preferences for how the builder itself looks while you work — light or dark theme, and page zoom. These are local to your browser and have no effect on your exported page.

3. Data Connections Tab

Configure real API connections and map their responses onto global variables your widgets can use — found in the Data Connections tab.

Configuring a connection

  • URL & method — GET, POST, PUT, or DELETE.
  • Headers — with common presets available.
  • Query Strings — always appended to the URL, regardless of method.
  • Body Parameters — sent as a query string for GET, or as the request body for everything else.
  • Additional JSON Body — raw JSON text; an object merges with your Body Parameters (parameters win on a name clash), an array is sent exactly as written and Body Parameters are ignored.
  • Both headers, query strings, and the additional JSON body support {{variableName}} placeholders, substituted from your current global variables at request time. An unresolved placeholder is left as literal text — handy for spotting a typo'd name.

Three Quick Start presets (generic GET/POST/PUT) can pre-fill a whole connection to start from, and "Import from Gateway" can merge in connections and variables from a JSON file.

Triggers

TypeBehavior
CyclicFetches automatically on a fixed interval you set.
ConditionalWatches a variable and fires only the moment it becomes true (not repeatedly while it stays true).
ManualNever fires on its own — call it from a widget with await apiConnections['ConnectionName']().

Every enabled cyclic or conditional connection also fires once shortly after the page loads, so you're not stuck waiting for the first full interval to see a value.

Mapping the response

Each connection has a list of variables, each defined by a dot-separated path into the JSON response (e.g. data.items.0.result — a numeric segment indexes into an array). Leave the path empty to capture the whole response body. Use Auto-populate Variables after a successful test to generate this list automatically from the response shape.

Testing & CORS

The Test API button runs your connection immediately and shows the raw response, with detailed troubleshooting notes if it fails (by status code, or by connection-level network errors). If an API doesn't allow direct browser requests, the Use CORS Proxy checkbox routes the request through a public proxy — fine for quick testing, but not something to depend on for a production deployment; for that, handle CORS on the API side or run your own proxy.

4. Variables Tab

A flat, direct editor over every global variable in your project — whether you created it by hand, it was populated by a data connection, or a widget created it itself at runtime. Found in the Variables tab.

Browsing & filtering

The search box filters by name as you type. Two dropdowns narrow the list further: one switches between All Variables and just the ones a data connection defines; the other filters to a single Group (see below). Variables are listed under a collapsible header per group — click a group's own arrow to collapse or expand it.

Creating a variable

+ Create Global Variable opens a small form: a name, a Type (String, Number, or Boolean — this only changes how the Initial Value field parses what you type, not a hard constraint enforced later), an initial value, an optional description, and a Group (type an existing group name to add it there, or a new one to create it on the spot — not limited to the three built-in groups below).

Editing, renaming & deleting

Edit turns a row's name and value into input fields in place. Changing the value alone just updates it. Changing the name does something more thorough: every window's code that reads the old name (as globalVariables.oldName, window.oldName, or the bare identifier) is rewritten to the new name too, everywhere in your project — not just the entry in this list — so nothing silently starts reading undefined. Delete removes the variable from this list and from the live runtime immediately; any widget still reading its name afterward just sees undefined, the same as reading a name that was never defined.

A row can also be dragged to reorder it within its group — cosmetic only, it doesn't affect anything at runtime.

Where variables come from

Every variable in this list falls into one of three groups, shown as the Group column:

  • Colours — automatic for any variable named global_style_<number>, the slot convention the Colour tool's own "+ New Tag" (and every built-in template) uses.
  • API — a variable defined by a Data Connection's own variable mapping, whether or not it's been populated by a real response yet (see the Data Connections tab above) — shown as Pending here until the connection actually runs once.
  • General — everything else, including anything you type into this tab's own Create form, and any global_text_*/global_font_* tag the Text/Font tools create.

A variable a widget creates itself at runtime with a plain globalVariables['name'] = value assignment (see the Runtime Reference above) also shows up here automatically, usually within a second or two, since this list is kept in sync with the live runtime rather than the other way around.

The alarm variables below are tagged into their own Alarms group.

Alarms

Alarm objects are defined in their own Alarms tab, next to Variables at the top of the Variables tab. Alarms are one big editable table — one row per alarm. Click + Add alarm at the bottom to add a row, edit any cell in place (Enter or clicking away saves it, Esc reverts), and click × to delete a row. The Table / Sheet / JSON toggle gives two more views of the whole list. Sheet is an Excel-style grid of plain text cells (Name, Trigger, Variable, Condition, Value / Bit #, Alarm Text, Ack) — paste rows copied from Excel or Google Sheets into any cell (a copied header row is skipped; the same paste also works in the Table view, landing at the row and column you pasted into, and an invalid paste is rejected with a message instead of half-applied), use Add rows (type how many) for more blank rows, or Copy as tab-separated to go the other way; Enter/arrow keys move between rows. JSON is the raw text. In both, valid JSON applies as you type or paste; broken JSON stays in the box with an error so you can fix it, and only when you switch back to Table is it discarded — you get a message and the last valid alarms remain. Columns:

  • Name — unique; also the id used to acknowledge it.
  • Alarm Text — what alarm displays show.
  • Trigger, Variable, Condition and Value / Bit — what makes it fire, always against a Variable you pick (the cell suggests every existing one; an empty one is outlined because it never fires):
    • Variable threshold — greater than / at least / less than / at most a number.
    • Bit — a single bit (0–31, 0 = least significant) of an integer variable: put the bit number in Value / Bit # and pick is 1 or is 0 as the Condition (e.g. bit 1 is 1). A plain true/false variable counts as bit 0.
    • Value — the variable equals (or doesn't equal) a given value, compared as text so 1 and '1' match.
  • Ack Required — Yes: the alarm needs acknowledging. Acknowledge it while it is still active and it stays listed, marked acknowledged, until the condition clears; if the condition clears first it stays listed as "back to normal" until someone acknowledges it. No: it leaves the queue by itself the moment the condition clears.

Alarms are a regular variable. Every alarm lives in one array called alarms — nothing special-cased — so it's saved, exported (HTML, JS, and JSON) and imported exactly like any other variable, shows up in the table below as an ARRAY, and can be seeded straight into a project file:

"alarms": [
  { "name": "Tank High",
    "trigger": { "type": "threshold", "variable": "tank_level", "operator": ">", "threshold": 80 },
    "text": "Tank level above 80%",
    "ackRequired": true }
]

Insert Alarm Tracker adds a transparent, invisible background widget to the current page (named "Alarm Tracker", parked top-left — it draws nothing) on its own "Alarm Tracker" layer at the very back. Leave that layer visible — hiding a layer stops the widgets on it, which would stop the tracker. The widget itself draws nothing, so there's nothing to see. It is what actually evaluates your alarms: every half second it checks each trigger and keeps the ones currently needing attention queued, newest first, in the alarm_queue variable. A project only needs one; the button turns into "✓ Alarm Tracker added" once there is one. The same widget is also in the template library (JavaScript tab → Alarm Tracker).

Each queue entry is { id, name, title, subtitle, text, ackRequired, active, acked, time } (active: false = back to normal, awaiting acknowledgement; acked: true = acknowledged but still active) — title/subtitle are exactly what the Alarm Display widget already renders, so pointing its VARIABLE_NAME at alarm_queue works as-is, and its ACK button counts as an acknowledgement. To acknowledge from your own code (or an Events tool "Change a value" action), set alarm_ack to an alarm's name, or '*' for all; the tracker clears it again after acting. For simple displays the tracker also keeps alarm_count (number queued) alarm_active (true while any queued alarm still needs attention, i.e. isn't acknowledged — point an Alarm Banner at it) and alarm_history (see below) up to date.

The Alarm Log widget (Basic Pack, JavaScript tab) is the traditional table-style display for the queue: one row per queued alarm with time, state (ACTIVE, or RTN = back to normal but still waiting for acknowledgement), alarm name and description, newest first, with a per-row ACK button for alarms that need acknowledging and an ACK ALL button. A state of ACKED means acknowledged while still active — the row stays, dimmed, until the condition clears. It reads alarm_queue by default (change the alarm_queue entry in its SERIES list at the top of its code to point it elsewhere) and shows sample rows until a queue exists.

The Alarm History widget (Basic Pack, JavaScript tab) shows everything that has happened: the tracker records every RAISED, ACKNOWLEDGED, RETURNED (back to normal) and CLEARED event, with the value that was read at the time, in alarm_history (newest first, latest 500, started empty each session). The widget is a read-only table of that list — it never shows sample data and can't clear the history.

Like every widget, the tracker only runs while the Builder tab is open in the editor, and always in an exported page. An alarm acknowledged while it is still active is not raised again until its condition has cleared and then come true again.

5. Editor Tab

A dedicated code editor for one widget at a time, with a live preview alongside — useful when you want a bigger, distraction-free editing surface than the canvas provides. It's the exact same underlying widget data as the canvas: edit here or on the canvas, and both stay in sync. Found in the Editor tab.

Picking a widget

The left column lists every JavaScript and HTML widget on the current page (Image widgets have no code, so they don't appear here). Click one to open it; arriving via a widget's own Open in Editor button (Properties panel or canvas toolbar) jumps straight to it.

The code editor

Syntax-highlighted, with a live rendered preview alongside so you can see the effect of a change without switching back to the canvas. For a JavaScript widget, editing here doesn't re-run it automatically — same as everywhere else this code lives — apply changes the same way (▶ Run/Edit on the widget, or from the canvas). An HTML widget's preview updates as you type instead, no run step needed. Drag the vertical handle between the preview and the editor to resize either side.

Colour/Text/Font/Tags/Events toolbar

The same five tools described in the Buttons & Controls Reference above, shown here in the code editor's own header whenever a JavaScript widget is open — acting on that one widget specifically, with no selection needed.

Runtime Reference panel

A collapsible column on the right edge holds the same live, searchable Runtime Reference mentioned above (functions, plus every current variable's name and value, filled in for your actual project). It can be pinned open as a persistent column instead of a collapsible one — the pin state is shared with the identical panel in the Properties panel on the canvas, so it stays open (or closed) consistently wherever you're editing code.

6. AI Context Tab

A copy-ready brief describing this app's project file format, meant to be pasted into an external AI chat so it can generate a valid project for you to import — useful if you'd rather describe what you want in words than build it by hand. Found in the AI Context tab, which also has a "How to use this" walkthrough of the same round trip described below.

Example

Click Copy Markdown, paste it as the first message to an AI chat (ChatGPT, Claude, or similar), then describe what you want in the same message or a follow-up — for example:

Build me a single JavaScript widget: a digital clock, 300x80px,
showing HH:MM:SS in a monospace font, text color bound to a global
variable so I can theme it later. Give me the full project JSON.

The brief tells the model the exact shape this app expects, so a reasonable response comes back as a real, importable project — something like this trimmed down to one widget:

{
  "globalFonts": {},
  "builderPages": [{
    "id": "page-1",
    "name": "Home",
    "windows": [{
      "id": "window-1",
      "title": "Digital Clock",
      "type": "javascript",
      "content": "",
      "jsCode": "const TEXT_COLOR = (window.globalVariables?.global_style_1) || '#1a1a1a';\nfunction tick(){ container.textContent = new Date().toLocaleTimeString(); }\nsetInterval(tick, 1000); tick();",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 300, "height": 80 }
    }],
    "backgroundConfig": { "width": 800, "height": 600, "color": "#ffffff" },
    "layerStates": [{ "layerNumber": 1, "visible": true, "name": "Layer 1" }]
  }],
  "dataConnections": { "apiConnections": [], "globalVariables": { "global_style_1": "#1a1a1a" } },
  "exportType": "multi-page",
  "totalPages": 1
}

Save that response as a .json file and use File → Load From File to bring it in — or paste it straight into a new chat's response and copy it from there. If the model gets a field wrong, importing will either ignore what it doesn't recognize or throw a clear error rather than silently corrupting your existing project; try again with the error message pasted back in.

7. Exporting Your Project

Everything you build lives only in your browser until you export it. Available from the File menu — see the Buttons & Controls Reference above for exactly where each control sits.

File → Save (JSON project save/load)

The only format that round-trips back into the builder. Use this as your real save file — save it when you want to preserve your work, and use Load From File to bring it back in later, on this computer or another one. The builder also keeps a best-effort autosave of your last session in your browser, but that's a safety net for an accidental tab close, not a substitute for saving.

File → Export: HTML

The main deliverable — one fully self-contained .html file containing every page, all your widgets, and the complete data-connection runtime, inline. Open it directly in a browser or embed it as an iframe anywhere. This is a one-way output for deployment, unlike the JSON save above — you can't load an exported HTML file back into the builder to keep editing it.

File → Export: JS

A .js-only bundle meant to be pasted into markup you already have, rather than opened on its own.

Both HTML and JS exports have two extra options next to them:

  • Strip Comments — removes comments from each widget's own code in the exported output. Doesn't apply to the JSON save, since that's meant to still be readable/re-editable later. Useful when the export is headed for something with limited storage, like a microcontroller's flash.
  • Gzip — compresses the downloaded file further (compression is dramatic here — a real 15-widget test page went from 315KB raw to 21.7KB gzipped). Comes with a Format choice: Readable Text (.h) gives you a hex byte-array (also copied to your clipboard) to paste straight into an Arduino sketch or other C/C++ firmware source with no filesystem needed; Raw Binary (.gz) gives you the actual gzip file, for serving a pre-gzipped file from a filesystem or static web server with a Content-Encoding: gzip header.

Every export format has a matching Preview option that opens the result in a new tab before you download anything.

8. Tips & Good Habits

Export JSON often Name your pages before wiring nav Use myWindow in a widget's own code Check the Variables tab when stuck
  • Export a JSON save file regularly. The in-browser autosave is a convenience, not a backup — clearing your browser data or a storage limit can lose it.
  • Use the System/API variable inspector templates (in the JavaScript template library) while building — they render a live table of every current global and API variable, which is the fastest way to confirm a value is actually what you think it is.
  • Prefer myWindow over a widget's own name when writing that widget's own code — it keeps the code copy-paste-safe if you ever rename the widget or duplicate it.
  • A hidden layer stops its widgets from running, not just from showing — useful for pausing something you're not currently working on rather than deleting it.

9. Guide: A Dashboard Without a Backend

Why this builder doesn't have a server, what that actually buys you, and the real tradeoffs worth knowing before you rely on it.

Most dashboard and HMI tooling assumes a server somewhere: something that holds your layout, proxies your data, and serves the page to whoever opens it. This builder skips that tier entirely. Everything — the canvas you design on, the widgets you drop onto it, the API calls those widgets make, the exported file you end up with — runs as plain JavaScript in one browser tab. There's no account, no project stored on a server, and no backend process that has to stay up for your dashboard to work.

That's a deliberate scope decision, not a missing feature. A lot of small dashboards — a status board for a workshop, a live view of a handful of sensors, a panel embedded in an internal tool — don't need a hosted backend, a login system, or a recurring hosting bill just to arrange some widgets and point them at data that already exists somewhere. The output is a single, portable HTML file you own outright.

How the data actually flows

A Data Connection you configure in the builder doesn't route through any server of ours — it's a direct fetch() call made by the visitor's own browser, straight to the URL you typed in. The response gets mapped into globalVariables (via each variable's jsonPath), and every widget on the canvas reads from that same shared bag of values:

// Inside any JavaScript-type widget
const temperature = globalVariables['boiler_temp_c'] ?? 0;
myWindow.querySelector('.reading').textContent = temperature.toFixed(1) + '°C';

When the page re-polls the connection (on whatever trigger you picked — cyclic, conditional, or manual), globalVariables updates in place and anything reading it on its own next tick picks up the new value. There's no intermediate store, cache layer, or message broker — just a fetch, a JSON-path extraction, and a plain object every widget already has access to.

When you still need a backend

Honesty matters more than a sales pitch here: this architecture has real edges.

  • CORS. Since the request comes from the visitor's own browser, the API you're calling has to allow cross-origin requests from wherever the exported page is hosted. An internal API on the same network as the device viewing the dashboard is usually fine; a third-party API that doesn't send CORS headers will simply refuse the request, with no way around it from the browser alone.
  • Secrets. Anything in a widget's code or a connection's headers — including an API key — ships inside the exported HTML file, readable by anyone who views source. Don't put a secret you can't afford to expose into an exported project; use a key scoped to read-only, low-sensitivity data, or front it with your own lightweight proxy if it needs to stay private.
  • Multi-user shared state. Every visitor's browser holds its own copy of globalVariables. If you need two people looking at the same dashboard to see each other's changes in real time, you need a real backend (or a third-party realtime service) behind the API you're connecting to — this builder has no concept of a shared server-side session.

If none of those apply to what you're building, the no-backend approach is usually the faster, cheaper path — and it's why the export is a single file instead of a deployable service.

10. Guide: JSON Project Schema

The exact shape of a project's JSON — the same format File → Save writes and File → Load From File reads. Useful if you're generating a project programmatically or with another LLM (see the AI Context tab above for a prompt-ready version of this).

The top-level shape

A saved project is one JSON object with four top-level pieces: the list of pages, which page is active, a display name, and the shared data-connection state.

{
  "builderPages": [ /* array of page objects, see below */ ],
  "activeBuilderPageId": "page-1",
  "appName": "My Dashboard",
  "dataConnections": {
    "apiConnections": [ /* array of API connection configs */ ],
    "globalVariables": { "boiler_temp_c": 61.4, "pump_running": true }
  }
}

Each entry in builderPages is one page/tab in the builder:

{
  "id": "page-1",
  "name": "Page 1",
  "windows": [ /* array of window objects, see below */ ],
  "backgroundConfig": { "width": 1200, "height": 800, "color": "#ffffff" },
  "layerStates": [
    { "layerNumber": 1, "visible": true, "name": "Layer 1" }
  ]
}

dataConnections is global and shared across every page — unlike windows and layerStates, which are per-page.

A window object

A "window" is one widget placed on the canvas. Its shape depends a little on type, but every window shares the same positioning fields:

{
  "id": "window-172...-ab12c",
  "title": "Boiler Temp",
  "type": "javascript",
  "jsCode": "const el = document.createElement('div'); ...",
  "content": "",
  "position": { "x": 40, "y": 40 },
  "size": { "width": 240, "height": 120 },
  "layer": 1,
  "showCodeInExport": false,
  "isOffCanvas": false
}

type is one of "javascript" (runs jsCode in a sandboxed context with globalVariables/myWindow available), "html" (renders content as static markup in an iframe, themed once at load), or "visualization" (an image/media window). A hand-written or generated project only needs to get these fields right for File → Load From File to open it correctly — there's no schema validation step beyond that.

11. Guide: Connecting a Real API

A worked example, start to finish — wiring a real JSON API response into a widget on the canvas.

A worked example

Say your API returns this when you hit it:

{
  "status": "ok",
  "readings": {
    "boiler": { "temp_c": 61.4, "pressure_bar": 2.1 }
  }
}

In the Data Connections tab: add a new connection, set its URL and method (GET for most read-only APIs), then add one variable per value you want available to your widgets. Each variable takes a name (what you'll call it in globalVariables) and a jsonPath pointing into the response above:

Name: boiler_temp_c      jsonPath: readings.boiler.temp_c
Name: boiler_pressure    jsonPath: readings.boiler.pressure_bar

Pick a trigger — cyclic re-fetches on an interval (the normal choice for a live dashboard), conditional only fetches when some other variable meets a condition you set, and manual only fetches when a widget explicitly calls the connection's trigger function. Once it's saved and running, any widget can read globalVariables.boiler_temp_c directly — no further wiring needed on the widget's side.

When the response never arrives

If a connection never seems to update, check in this order: open the browser's own DevTools Network tab and confirm the request is actually firing (wrong trigger setup is the most common cause); check the response status isn't an auth error (a missing or wrong header); and check the console for a CORS error specifically — that shows up as a failed request with no real HTTP status, and means the API itself needs to allow your page's origin, which isn't something fixable from this builder alone (see the no-backend guide above for why). A quick way to isolate the problem: paste the exact URL into a new browser tab — if it loads there but not in the builder, it's almost always CORS.

12. Guide: Exporting, Embedding & Deploying

Where an exported file actually goes once you have it.

Static hosting

The exported HTML file is a complete, self-contained static page — it needs nothing but a place to be served from. Drop it onto any static host (Vercel, Netlify, GitHub Pages, S3, or your own web server) and link to it directly. There's no build step and no server-side dependency to install; the file that downloads from File → Export → Complete HTML is exactly what gets deployed.

Embedding in an existing page

To drop a dashboard into a page you already have, host the exported file somewhere and embed it with a plain iframe:

<iframe src="/dashboards/boiler.html" width="1200" height="800" style="border:0"></iframe>

Because the export is fully self-contained (its own styles, its own data-connection runtime), it won't collide with the embedding page's own CSS or JavaScript — the iframe boundary keeps the two completely separate.

Flashing to a microcontroller

For a kiosk-style HMI running off an ESP32, Raspberry Pi Pico W, or similar device serving its own small web server, use the Gzip Output option next to Export (see the Exporting guide above) with the Readable Text (.h) format. That gives you a plain C byte array —

const unsigned int INDEX_HTML_GZ_LEN = 8213;
const unsigned char INDEX_HTML_GZ[] = { 0x1f, 0x8b, 0x08, ... };

— that pastes directly into an Arduino sketch or other firmware source and gets served with a Content-Encoding: gzip response header, with the browser doing the decompression. No filesystem on the device is required at all; the whole dashboard lives in flash as a byte array.

Privacy Policy Terms