THE FIELD GUIDE

Using components

Build, style, and script the current component library.

This guide describes the current Builder and its 88 toolbox entries. The component reference lists every implemented control, contextual inspector property, range, C# property, constructor default, and declared method/event. The roadmap tracks features still pending.

Contents

Build a form

  1. Run dotnet run --project src/Forma.Builder from the repository root. The project uses Windows, .NET 10 and WebView2.
  2. Drag from Toolbox to the form, or double-click a toolbox entry to insert it. Search filters the toolbox.
  3. Select each control and set a useful Name, such as nameInput or greetingLabel. Generated names use the overall design sequence; a newly added TextBox is not necessarily textbox1.
  4. Drag visual controls to move them; use selection corners/edges to resize. Nearby alignment/spacing guides help snapping. Canvas zoom changes display scale.
  5. Edit Properties on the right. Containers own children dropped into their content area; moving a parent moves its children.
  6. Preview opens the running design in a separate resizable/maximizable window. Its tree is a clone: typing and runtime scripts do not change the saved design. Close and reopen Preview to restart with your latest edits.
  7. Save a .forma file. Open restores the tree, appearance and custom sources. Local images are embedded on save (10 MB per image limit). Undo/Redo history is session-only. Ctrl+Z/Ctrl+Y change the design; focused editable fields retain native text undo.

Timer, BackgroundWorker, Tooltip, Toast, LoadingOverlay, context menus and dialogs appear as named tray items below the form. They do not occupy canvas rectangles but can target visual controls or create runtime UI.

Properties and layout

SurfaceExamplesWhere to change
Builder inspector / AppearanceFont size, Foreground, Width, LockedInspector; visual overrides in custom CSS
Runtime JavaScript APItext, value, checked, selectedTabSupported forma.get / forma.set operations below
Core C# modelItems, Rows, IconName, DescriptionObject properties in a C# host project

These overlap but are not interchangeable. NumericUpDown uses inspector number, C# Value, and JavaScript value. A field appearing in the inspector does not automatically make it a runtime API setter.

Name is a script lookup name; ID is immutable identity. Lookup accepts an exact Name or ID and requires one match. Names are case-sensitive. A target picker resolves a selected visual control to its ID; C# sets TargetId = target.Id.

X/Y are pixels in the parent content area, below headers. Width/Height and min/max dimensions are pixels. LayoutSlot is one-based for panes, tabs and cells. Font size is pixels; Opacity is percent. Per-side margin/padding fields control spacing. Enabled/Visible affect Preview; controls stay selectable in Design. Locked prevents designer edits. Contextual layer actions change stacking; custom Z-index is inside Custom Properties.

FlowLayoutPanel uses child order/orientation instead of free X/Y; drag to reorder. TableLayoutPanel uses Columns/Rows and cell placement. SplitContainer horizontal means left/right panes, vertical means top/bottom; splitter dragging is pending. TabControl supports horizontal/vertical headers; use Add tab, select a page, then drop its children. Tab selected index is zero-based; a child's LayoutSlot is one-based.

Dock and Anchor

Dock and Anchor are in Layout for visual children of free-position containers. Dock values are none, top, bottom, left, right and fill. Edge docks reserve space in child order; Fill uses what remains. Resize an edge-docked control along its free axis to change its thickness. Dock controls its position and the other dimension. Use one Fill control per content area.

Anchor defaults to top,left. top,right keeps the right-edge distance; bottom,right keeps the bottom/right distances. top,left,right stretches width, and top,bottom,left,right stretches both dimensions. An axis with neither edge anchored stays centered on that axis. Dock takes precedence over Anchor. These settings persist and are undoable, and also work as Preview containers resize. Managed stack/flow/table layouts control child placement, so Dock/Anchor are disabled there. Use a Panel inside a managed layout for freely positioned or anchored content. AppShell-controlled dimensions are also disabled in the inspector and their resize handles are hidden.

forma.set("sidebarPanel", "dock", "left");
forma.set("mainPanel", "dock", "fill");
forma.set("submitButton", "anchor", "bottom,right");
const dock = forma.get("mainPanel", "dock");

Resizing managed children changes their size without changing their stored X/Y or applying relative left/top offsets. Dragging still reorders them. In free-position containers, hold Alt while dragging to bypass alignment snaps.

Custom Properties editor

Component JavaScript is now script.js. For project-wide state/functions and forma.provide/use, see global scripts and shared state. Open the shared file through Project → Main script.

Select a component → Custom Properties, directly below Search properties. Edit:

  • CSS: scoped visual overrides using :host.
  • Script: JavaScript executed once per Preview start.
  • Events: exported handler methods in event.js, subscribed from script.js.
  • Custom JSON: an object available as component.properties.

The built-in editor opens in a component Code workspace tab. Switching back to Design preserves its draft, selected file, cursor and undo history. Use Ctrl+Space for Forma API, component names, supported properties, event payloads and custom values. Syntax diagnostics show underlines, gutter markers and a Problems list. Unknown literal component names and unsupported runtime properties show warnings; dynamic values and behavior still need Preview testing.

The built-in editor has line numbers, syntax colors, bracket matching, folding, indentation and search (Ctrl+F). Format / Ctrl+Shift+F formats the current tab. Format on save (enabled by default) formats all four sources before saving; turn it off to skip formatting. Syntax errors stop formatting without replacing your text.

Save / Ctrl+S automatically applies sources. External editor (under ···) uses detected VS Code or an executable chosen through Choose editor. External saves auto-apply while the component editor remains open. Invalid JSON keeps the last valid version. Reopen Preview after behavior changes because an existing Preview owns an independent copy.

Right-click a component and choose View CSS, View Script, View Events, or View Custom Properties. Code opens in its own workspace tab; switching to Design preserves the draft. Each source has a file tab and an unsaved-change indicator.

Files are component.css, script.js, event.js, custom-properties.json. Saved designs use <project-name>.components/<component-id> beside the project. Unsaved designs use %LOCALAPPDATA%/Forma/ComponentEditors. Applied sources are also embedded in .forma.

Use the Events inspector to create handler methods in event.js and connect them from script.js. forma.root addresses the active form; event source/subscriber handles address individual components. Input/Change supply live property values, and PropertyChanged supplies observed runtime changes with previous values and origins. See events and payloads for the complete contract.

Custom JSON must be an object:

{ "greeting": "Hello", "clicks": 0 }
forma.on("click", () => {
  component.properties.clicks += 1;
  forma.set("greetingLabel", "text", `${component.properties.greeting}: ${component.properties.clicks}`);
});

Runtime JSON changes reset when Preview restarts. component.characteristics remains a compatibility alias for the same object.

:host { font-size: 20px; color: #1f2937; }
:host:hover { background-color: #eff6ff; }
:host input { border-color: #2878ff; }

Every selector must begin with :host. Regular rules, @media and @supports are supported; global selectors and standalone keyframes are not. Declarations take priority over inspector appearance. Boilerplate copies current styles: remove/edit a declaration if you want subsequent inspector changes to control it. Toast/LoadingOverlay popup CSS also matches its source component's :host. Keep geometry in Layout properties.

JavaScript API

Script is JavaScript, not C#. It receives forma and component; api remains a compatibility alias. It does not directly receive Core model objects or the WinForms window.

For live values declared outside events, use const name = forma.bind("nameInput", "value") and read name.value. forma.ref, forma.reactive, forma.computed, forma.effect, and forma.watch support local reactive state and automatic updates. See reactive state and bindings for examples, subscriptions, cleanup, and write semantics.

APIPurpose
forma.on(event, handler)Listen on this component DOM element; automatic listener cleanup
forma.get(nameOrId, property)Read one of the supported properties below
forma.set(nameOrId, property, value)Send a supported update to the cloned Preview model
forma.bind(nameOrId, property)A live property binding; read its .value, and write only supported writable properties
forma.ref(value) / forma.reactive(object)Reactive local values or a shallow reactive object
forma.computed(getter) / forma.effect(callback)Derived values and reactive work
forma.watch(source, callback, options)Observe changes with automatic script cleanup
forma.provide(name, value)Register a shared module from the global script
forma.use(name) / forma.sharedAccess a shared module or the session's shallow reactive namespace
forma.find(nameOrId)Get the matched DOM element for advanced local work
forma.showDialog(nameOrId)Open Dialog/ConfirmationDialog
forma.showForm(formName, { modal })Open another form of the project in its own Preview window; modal: true blocks the opener like ShowDialog. Returns a promise for the value passed to closeForm (rejects if the form does not exist)
forma.closeForm(result)Close this form's window and resolve the opener's showForm promise with result
forma.showToast(nameOrId) / forma.closeToast(nameOrId)Notification lifecycle
forma.cleanup(callback)Register cleanup for timers, extra listeners or resources

component.id, component.name, component.element, component.properties describe the current component. forma.on listeners run while the source is Enabled and Visible. Sync/async handler errors are reported in the Preview title. Script never runs in Design.

Runtime property operations

See the extended runtime property guide for shared appearance/geometry fields, input settings, display controls, dialogs/pickers, structured data and read-only status properties. These now support get/set/bind with the types shown there and in code suggestions.

Use exact camelCase keys. forma.set uses the bridge; it is not a synchronous DOM assignment. A getter immediately following a setter in the same callback may see the previous state. Keep the new value in a local variable if you need it immediately.

Keyforma.get supportforma.set supportValue
sourceImage/PictureBox/Avatar image sourceImage/PictureBox/AvatarAbsolute local file path, file/http/https URL, image data URI; empty string clears
selectedPathFilePicker/FolderPicker displayed selected pathNot supportedString; read after Browse finishes
textDisplay/state text; use value for live inputAll modelsString, max 32767 characters
enabled, visibleAll controlsAll controls; enabling Timer also starts/stops itBoolean
intervalTimerTimerInteger milliseconds, clamped to 10–3600000; use 1000, not "1000"
minimum, maximum, incrementNumericControl descendantsSame typesFinite numbers; increment must be positive; range edits clamp Value
speedSpinnerSpinnerInteger milliseconds, clamped to 100–5000
shape, linesSkeletonSkeletontext/rectangle/circle; integer lines clamped to 1–10
checkedCheckBox, RadioButton, ToggleSwitch, ToggleButtonSame typesBoolean
valueLive text inputs; numeric controls including ProgressBar/CircularProgress; date/time/color input valuesNumericControl descendants, DateTimeInput descendants, ColorPickerFinite number or correctly formatted string
selectedIndexComboBox/ListBox/ListView native selectsSame typesZero-based integer; -1 clears
itemsChoice and multiple-choice controlsSame typesString array
selectedIndexRadioGroup/SegmentedControlSame typesZero-based integer; -1 clears
checkedIndicesCheckedListBox/CheckBoxGroupSame typesInteger array
value, readOnlyRatingRatingWhole-star value, boolean
selectedTabTabControl, AccordionSame typesZero-based integer
selectedRowDataGridViewDataGridViewOriginal Rows index; -1 clears
columnsDataGridViewDataGridViewArray of header strings
rowsDataGridViewDataGridViewArray of string-cell arrays
readOnly, sortingEnabled, filteringEnabledDataGridViewDataGridViewBoolean
filterTextDataGridViewDataGridViewString
sortColumnDataGridViewDataGridViewZero-based column; -1 unsorted
sortDirectionDataGridViewDataGridViewascending, descending
isActiveSpinner, LoadingOverlay, SkeletonSame typesBoolean
variant, position, duration, dismissibleToastToastSeverity string, corner string, milliseconds, boolean
isOpenToast, Dialog/ConfirmationDialogNot supportedBoolean; use lifecycle helpers to open/close

For a textbox: read value, write text. forma.set("nameInput", "value", "...") is not supported. Numeric setters include ProgressBar/CircularProgress. Date/time formats are below.

DataGridView supports columns (string array), rows (array of string arrays), readOnly, sortingEnabled, and filteringEnabled through get/set. Use its row helpers to append, update, remove, or clear runtime data. Additional supported properties include typography, Badge variants, popup targets, descriptions, icons, Pagination state, TreeView nodes, PropertyGrid entries and RichTextBox documents. Use the runtime property guide for their exact types and read-only restrictions. Pass structured arrays directly, rather than JSON strings. CheckedListBox uses checkedIndices; RichTextBox uses text and document. Arbitrary model fields are not automatically script properties.

For values shared across component scripts, use global scripts and shared state. A plain local variable remains local to its script; forma.use returns an explicitly provided module.

JavaScript recipes

TextBox to Label on click

Name the TextBox nameInput, Label greetingLabel, and attach this to the Button:

forma.on("click", () => {
  const name = String(forma.get("nameInput", "value") ?? "").trim();
  forma.set("greetingLabel", "text", name ? `Hello, ${name}!` : "Enter your name first.");
});

Copyable source. To fill/reset the TextBox, use forma.set("nameInput", "text", "").

Live update

Attach this to the TextBox:

forma.on("input", () => {
  forma.set("greetingLabel", "text", component.element.value);
});

Copyable source. Debounce expensive work that does not need to happen on each character.

LoadingOverlay and Toast

Name tray components busyOverlay and savedToast. Configure overlay Target (blank covers the form), and Toast text/variant/position/duration. Attach to the Button:

let pending;
forma.on("click", () => {
  clearTimeout(pending);
  forma.set("busyOverlay", "isActive", true);
  pending = setTimeout(() => {
    forma.set("busyOverlay", "isActive", false);
    forma.showToast("savedToast");
  }, 1000);
});
forma.cleanup(() => clearTimeout(pending));

Copyable source. The delay demonstrates an indicator; replace it with your operation. Overlay blocks pointer interaction over its target; it is not a full keyboard-focus modal. Showing an already-open Toast does not restart its timeout; close then show for a fresh display.

Selection, visibility, progress

forma.on("click", () => {
  forma.set("greetingLabel", "visible", !forma.get("greetingLabel", "visible"));
  forma.set("notifications", "checked", true);
  forma.set("settingsTabs", "selectedTab", 1); // second page
  forma.set("workProgress", "value", 75);
});

Use your actual Names for label, checkbox/toggle, tabs and progress bar.

DataGrid selection

Attach to DataGridView:

forma.on("row-selection", event => {
  const row = event.detail.row;
  forma.set("rowLabel", "text", row < 0 ? "No row selected" : `Source row: ${row}`);
});

Copyable source. Rows remain identified by their original source indices after filtering/sorting. Another component can set the grid's filterText/sortColumn/sortDirection/selectedRow. Sorting/filtering does not reorder saved Rows.

Timer and dialog

Configure a tray Timer Interval and Enabled; put this on the Timer:

forma.on("tick", () => {
  forma.set("clockLabel", "text", new Date().toLocaleTimeString());
});

Put forma.on("click", () => forma.showDialog("confirmDelete")); on a Button. Configure the named Dialog's message/buttons. A JS promise returning this component's dialog result is not implemented; C# can subscribe to Closed. The Events inspector and event.js handler editor are implemented. For a separate project form with a promise result, use forma.showForm below.

Opening other forms

Preview starts with the form that is active in the designer and takes a snapshot of the whole project, so every form can be opened while it runs:

// form1: open the settings form as a dialog and use its result.
forma.on("click", async () => {
  const result = await forma.showForm("settingsForm", { modal: true });
  if (result?.saved) forma.set("statusLabel", "text", "Settings saved");
});

// settingsForm: hand a result back and close.
forma.on("click", () => forma.closeForm({ saved: true }));

Each form runs in its own window with its own copy of its controls, and main.js runs in each window so its forma.provide modules are available everywhere. forma.shared is one store for the whole run:

  • Assigning forma.shared.name = value in any form updates every open form, reactively.
  • A form opened later starts from the current values. While its main.js runs, assignments only fill values that no other form has set, so forma.shared.count = 0; initializes once instead of resetting the app.
  • Shared values must be JSON-compatible (numbers, strings, booleans, arrays, plain objects). Reassign to share a change (forma.shared.items = [...forma.shared.items, item]); mutating a nested object in place stays in that window. Functions, refs and reactive objects stay local to the window that assigned them.

Closing the first Preview window closes every window it opened.

Component families

Breadcrumb displays a selectable path; SideNavigation displays a single selected navigation option, vertically by default. Both use newline-separated Items and zero-based Selected index (-1 clears). SideNavigation also exposes Orientation. They support keyboard navigation and mark the selected item with aria-current="page". Their navigation event is an application hook, not an automatic URL change or page loader:

forma.on("navigate", event => {
  forma.set("pageTitle", "text", event.detail.text);
  console.log("Selected index:", event.detail.index);
});

Use forma.bind("sidebar", "selectedIndex") for a live selection and forma.set("sidebar", "items", ["Home", "Reports"]) for dynamic navigation data. The generated JavaScript template for these controls starts with navigate.

DropdownButton opens a command menu. SplitButton adds a separate main action with Primary enabled, leaving the dropdown available when that action is disabled. Both use Commands (JSON), including nested commands, separators, disabled items, and checkable entries. Down Arrow on the trigger opens the menu; Up/Down/Home/End navigate its top-level entries; Escape returns focus to the trigger. Outside clicks and command selection close it. Text updates retain the command tree. CommandButton is a regular action button with an icon, title, description, and optional visible text; it uses standard Click behavior.

For a SplitButton named runButton, put this in its JavaScript tab:

forma.on("primary-click", () => {
  forma.showToast("savedToast", { text: "Running the main action", variant: "info" });
});

forma.on("command-item", event => {
  console.log(event.detail.itemId, event.detail.text, event.detail.checked);
});

command-item callbacks are delivered after the native model accepts the command, with the actual checked state. Use this event for menu commands rather than a generic Click handler, which also sees clicks on the trigger and descendants. The generated script template chooses command-item for DropdownButton and primary-click for SplitButton.

Set Commands in the inspector with an array such as [{"Id":"save","Text":"Save"},{"Id":"autosave","Text":"Auto save","CheckOnClick":true}]. In JS use an actual array:

forma.set("runButton", "commandItems", [
  { id: "run", text: "Run once" },
  { id: "runAll", text: "Run all" }
]);
forma.set("runButton", "primaryEnabled", false);
const commands = forma.get("runButton", "commandItems");

Command getters return deep copies; setters validate the complete tree before applying it. JS commandItems also works for MenuStrip, Toolbar, ToolStrip, and context menus. CommandButton supports iconName, showText, description, and the common text/enabled/visible keys. All three controls support inspector editing, save/open, Undo/Redo, Preview scripts and bindings.

Chip is a selectable pill with Text, Variant, Checked, and optional Removable. Its close button removes it from the current Preview without deleting the saved design. forma.on("chip-remove", handler) reacts to dismissal. In C#, use Removed, Remove(), and Restore(). In JS, set isRemoved to false to restore it. ChipGroup supports multiple selected tags through Items and Checked indices; ButtonGroup supports a single selected option through Items and Selected index. Both groups support horizontal/vertical Orientation. ChipGroup arrow keys move focus without changing selection; Space/Enter activates the focused tag.

IconButton and FloatingActionButton use the bundled icon selector and Show text. Text supplies the accessible label even when the visible caption is hidden. FloatingActionButton is a circular elevated action button at its designed location; it does not automatically anchor itself to a window corner. Increase its width if you turn on Show text for an extended action button. Both expose standard Click behavior and work with disabled state.

const tags = forma.bind("tagPicker", "checkedIndices"); // ChipGroup
const mode = forma.bind("modeButtons", "selectedIndex"); // ButtonGroup
const selected = forma.bind("activeTag", "checked"); // Chip

forma.on("Click", () => {
  console.log(tags.value, mode.value, selected.value);
  forma.set("searchAction", "iconName", "settings");
  forma.set("searchAction", "showText", true);
});

Chip runtime properties: checked, variant, removable, isRemoved. Removal requires Removable=true; restoring does not. Icon action button properties: iconName, showText, and common text/enabled/visible. All five controls support inspector editing, save/open, Undo/Redo, scripts and bindings. Group selection changes preserve item elements and focus when Items have not changed.

RadioGroup and SegmentedControl provide a single choice from Items. Set Items one per line, Selected index (zero-based, -1 for none), and Orientation. RadioGroup renders native radios; SegmentedControl renders a button strip with arrow-key, Home, and End navigation. CheckBoxGroup permits multiple choices with comma-separated Checked indices (e.g. 0, 2) and Orientation.

Rating renders 1–10 stars, with Value from zero to the star count and optional Read only. Values round to whole stars. Click a star or use arrow keys/Home/End; Delete/Backspace clears the rating. Read only blocks user interaction while scripts can still set the value.

const plan = forma.bind("planPicker", "selectedIndex"); // RadioGroup/SegmentedControl
const interests = forma.bind("interestsPicker", "checkedIndices"); // CheckBoxGroup
const score = forma.bind("reviewRating", "value"); // Rating

forma.on("Click", () => {
  const plans = forma.get("planPicker", "items");
  console.log(plans[plan.value], interests.value, score.value);
});

Use forma.set(name, "items", ["A", "B"]) to replace choices, forma.set(name, "selectedIndex", 1) for a single selection, forma.set(name, "checkedIndices", [0, 2]) for multiple selections, and forma.set(name, "value", 4) for Rating. Reads return copies of arrays. Change events and reactive bindings reflect user selection. These controls support save/open, inspector editing, and Undo/Redo.

  • Button: Text, Style preset, common appearance; click behavior.
  • Label: display Text; update through text.
  • TextBox/SearchBox/PasswordBox/TextArea: starting Text, Placeholder, Read only, Password, Max length. Read live value. Password obscures presentation, not storage encryption.
  • MaskedTextBox: 0=digit, L=ASCII letter, A=letter/digit, *=non-underscore character; others are literals. Incomplete slots are _. Example 000-0000; Complete is read-only. Optional-mask syntax is pending.
  • RichTextBox: Edit content / Finish enables the formatting toolbar. Document supports paragraph/bullet/number blocks and bold/italic/underline runs. Paste imports plain text; plain Text replaces formatting. Arbitrary HTML/RTF import is pending.
  • LinkLabel: HTTP/HTTPS URL; Preview hands clicks to the native host/default browser. C# exposes LinkClicked.
  • CheckBox/RadioButton/ToggleSwitch/ToggleButton: Checked boolean. User radio interaction groups siblings with the same parent; explicitly set group states for programmatic changes.
  • ComboBox/ListBox/ListView: Items, Selected index; zero-based, -1 none. ListView is a list-style widget, not a multi-column grid.
  • CheckedListBox: Items and comma-separated zero-based Checked indices, e.g. 0, 2; C# SetItemChecked raises ItemCheck. JavaScript supports items and checkedIndices arrays.

Numeric, dates and loading

NumericUpDown/Slider: Minimum, Maximum, Increment, Number. ProgressBar/CircularProgress: Minimum, Maximum, Number. Numeric values clamp to range; C# uses double Value. CircularProgress displays a percentage. JS value setters also update progress displays.

DatePicker uses YYYY-MM-DD, TimePicker HH:mm, DateTimePicker YYYY-MM-DDTHH:mm without timezone suffix. Empty string clears them. ColorPicker uses #RRGGBB. C# properties are DateValue and Color; JavaScript reads/sets formatted string value.

Spinner: Active and speed (100–5000 ms). Skeleton: Shape text/rectangle/circle, Lines (1–10 for text), Active; rectangle/circle show one placeholder. They animate only in Preview and respect reduced motion. Your code decides when an operation is finished.

Containers and display

Panel is free-position; GroupBox adds a caption; Card adds Title/Description/Show header. SplitContainer has two panes; TabControl has names/selected tab/orientation/Add tab; FlowLayoutPanel packs/reorders with Gap; TableLayoutPanel uses Columns/Rows/Gap. C# parent.Add(child) owns a child; remove it from the old parent before reparenting. Parenting does not imply responsive docking.

Image/PictureBox/Avatar use Choose image; Source is read-only in the inspector. Clear image removes it. Contain fits; cover crops to fill; fill stretches. Local images embed on save. C# Source accepts file/data/loadable URL strings. In JavaScript, use forma.set("userAvatar", "source", imagePathValue) to load an image. text changes the name/fallback initials. Local paths are converted to file URLs in Preview; paths must exist and URLs must load. For a TextBox path, read forma.get("imagePathInput", "value"). For a FilePicker path, read forma.get("inputFile", "selectedPath") after browsing. Example on a separate Button:

forma.on("click", () => {
  const imagePathValue = forma.get("inputFile", "selectedPath");
  forma.set("userAvatar", "source", imagePathValue);
});

Avatar: Text (accessible name/automatic initials), optional Initials (up to four characters), Shape circle/rounded/square, image fields. Failed/empty images show initials. Badge: neutral/info/success/warning/danger Variant; follows inspector font size/weight. Divider: Orientation, Thickness 1–12, solid/dashed/dotted line, optional Text caption.

Icon: image/search/folder-open/square-check/circle/house/settings/lock/calendar/file-plus/list/x; Foreground sets SVG color, Stroke width 1–4. EmptyState: Text title, Description, Icon. Add a separate action Button if needed.

Tray feedback and workers

Toast: Text, Variant, corner Position, Duration 500–60000 ms, Allow dismissal. Show/close through JS or C#; open state is transient and notifications stack by corner.

LoadingOverlay: Text, Active, Target; blank Target covers parent (normally form). It lives in the tray and renders only in Preview. JS uses isActive, C# IsActive. It restores target aria-busy on cleanup.

Tooltip: Text, Target, Initial delay 0–10000 ms, Show duration 500–60000 ms, Placement. Hover/focus opens it; Escape/scroll/resize/removal/timeout closes it. Common ToolTip is a simpler native browser title.

Timer: Interval 10–3600000 ms and Enabled; starts in Preview and stops on disposal. BackgroundWorker: progress/cancellation flags, read-only Busy; C# runs work explicitly. Dropping a worker does not start a task, and JS has no RunAsync worker bridge.

Data, commands and pickers

DataGridView: Columns/Rows, Read only, sorting/filtering flags, Filter text, Sort column/direction, Selected row. Selection/cell edits use original row indices. Virtualization/grouping/binding/column dragging are pending.

TreeView: JSON Nodes with unique IDs, Selected node, comma-separated Expanded nodes. Pagination: Total items/Page size/Page (one-based); your app fetches/changes data on PageChanged. PropertyGrid: categorized string Entries plus whole-grid/per-entry read-only; object binding/typed editors are pending.

MenuStrip/Toolbar/ToolStrip/ContextMenu/ContextMenuStrip: command JSON with nesting, separators, disabled/checkable leaves. Toolbar orientation is configurable; context menus need a Target. StatusBar: left Text/Right text. Implement command actions yourself in C#. Use Dock explicitly for visual bars; default automatic docking and shortcuts are pending.

Dialog/ConfirmationDialog: title, message, Buttons (OK/OKCancel/YesNo/YesNoCancel), Allow cancel, read-only Result. They show messages, not arbitrary child form content. FilePicker/FolderPicker: Selected path, Dialog title; FilePicker also has a Windows Filter string. Builder supplies native dialogs; a custom C# host handles BrowseRequested and assigns SelectedPath.

FilePicker and FolderPicker

These are visual controls with a path display and Browse button. FilePicker selects a single existing file; FolderPicker selects a directory. Builder supplies the native dialogs in Preview. Inspector Choose file / Choose folder actions set the starting design path and support Undo/Redo.

Properties

Inspector fieldC# propertyApplies toUsage
NameNameBothUnique script lookup name, e.g. inputFile / outputFolder
TextTextBothBrowse button caption, e.g. Choose file…
Selected pathSelectedPathBothStarting path in Design; selected path in Preview; empty means none
Dialog titleDialogTitleBothTitle/description of the native chooser
File filterFilterFilePickerLabel/pattern pairs: Images|*.png;*.jpg|All files|*.*
Enabled / VisibleBuilder AppearanceBothWhether the picker can be used / shown in Preview

Filter entries alternate a label and a pattern separated by |. Separate multiple extensions with ;. Examples: Text files|*.txt, Images|*.png;*.jpg;*.jpeg|All files|*.*. FolderPicker has no extension filter. A path is selection state; choosing it does not read file contents, write a file, or create a directory.

JavaScript: read both selected paths

Name a FilePicker inputFile, a FolderPicker outputFolder, and a Label pathLabel. In Preview, use each Browse button first, then click a separate Button with this behavior:

forma.on("click", () => {
  const file = forma.get("inputFile", "selectedPath");
  const folder = forma.get("outputFolder", "selectedPath");
  forma.set("pathLabel", "text", `File: ${file || "None"}\nFolder: ${folder || "None"}`);
});

Copyable source. forma.get(name, "selectedPath") reads the displayed path. The setter for selectedPath is not implemented; use inspector/C# to set a path. A native selection does not dispatch a supported JavaScript SelectedPathChanged event yet; read after selection with a separate button. Do not read on the picker's Browse click and expect the new path before the dialog finishes.

C#: read, configure, and respond to selection

var file = new Forma.Core.Controls.FilePicker {
    Name = "inputFile",
    Text = "Choose image",
    DialogTitle = "Select an image",
    Filter = "Images|*.png;*.jpg;*.jpeg|All files|*.*"
};
var folder = new Forma.Core.Controls.FolderPicker {
    Name = "outputFolder",
    DialogTitle = "Select output folder"
};
file.SelectedPathChanged += (_, _) => label.Text = file.SelectedPath;
folder.SelectedPathChanged += (_, _) => label.Text = folder.SelectedPath;
// Read: string selectedFile = file.SelectedPath;
// Clear: file.SelectedPath = "";

label is your Label model. Add the pickers to the form with form.Add(file) / form.Add(folder). The compiled C# recipes include this pattern.

In a custom host, BrowseRequested asks your code to show a native chooser. For example, in a WinForms host handler, create an OpenFileDialog using file.DialogTitle and file.Filter; after DialogResult.OK assign file.SelectedPath = dialog.FileName. For a folder chooser use FolderBrowserDialog and assign folder.SelectedPath = dialog.SelectedPath. file.RequestBrowse() / folder.RequestBrowse() raises the request; it does not independently create a window. Cancel leaves the previous selection unchanged. Builder already supplies these handlers.

Full field/default tables: FilePicker reference, FolderPicker reference.

Structured property formats

Items/Tabs/Grid Columns use one item per line. Grid Rows is JSON arrays of string cells:

[["Ada", "10"], ["Grace", "2"]]

In JavaScript, DataGridView columns and rows are actual arrays rather than inspector strings. Use forma.set("gridName", "columns", ["Name", "Value"]) and forma.set("gridName", "rows", [["Ada", "10"]]) to replace data; forma.get returns copies. Atomic row helpers are forma.addRow, forma.updateRow, forma.removeRow, forma.clearRows, and forma.setCell. See grid scripting examples. Existing scripts using api remain compatible with the preferred forma name.

Tree nodes use unique IDs (maximum depth 24, 2000 nodes):

[{"Id":"root","Text":"Projects","Children":[{"Id":"forma","Text":"Forma"}]}]

PropertyGrid supports up to 200 entries; values are strings:

[
  {"Name":"Name","Value":"Forma","Category":"General"},
  {"Name":"Version","Value":"0.1","Category":"General","ReadOnly":true}
]

Command items need stable unique IDs; a disabled parent disables descendants. A checkable leaf toggles then raises the item event:

[
  {"Id":"file","Text":"File","Items":[
    {"Id":"open","Text":"Open"},
    {"Id":"separator","Text":"","Separator":true},
    {"Id":"autosave","Text":"Autosave","CheckOnClick":true,"Checked":false}
  ]}
]

Rich document blocks use paragraph/bullet/number and plain-text runs:

[{"Kind":"paragraph","Runs":[{"Text":"Hello ","Bold":true},{"Text":"Forma","Italic":true}]}]

C# model code

C# belongs in a host project, not Script. Builder does not generate a C# application or provide a C# script editor yet. The Demo host loads WebView2, constructs a bridge and renders Core controls.

using Forma.Core.Controls;
var form = new Forma.Core.Form { Title = "Greeting" };
var input = new TextBox { Name = "nameInput", Text = "" };
var label = new Label { Name = "greetingLabel", Text = "Welcome" };
var button = new Button { Text = "Say hello" };
button.Click += (_, _) => label.Text = $"Hello, {input.Text ?? "friend"}!";
input.TextChanged += (_, _) => label.Text = input.Text;
form.Add(input);
form.Add(label);
form.Add(button);
// After your host creates WebView2Bridge:
// var renderer = new Forma.WebView2.WebView2Renderer(bridge);
// await renderer.InitializeAsync();
// await renderer.RenderAsync(form);

Compiled C# recipes cover greeting, layouts, numeric/progress coupling, choices/checklists, grid/tree/property grid, rich text, dates/colors, feedback/commands and modern display. Validate with dotnet build docs/examples/Forma.Documentation.Examples.csproj. Recipes create model trees; the host supplies the window/rendering and styling/appearance.

Builder appearance is Forma.Builder.Appearance in BuilderViewModel.Appearance[control.Id]. Core models do not all have FontSize/Visible. In Builder code, ExecuteEdit makes undoable edits; its property action targets SelectedControl. Most model setters notify the renderer; auto-properties such as TextBox.ReadOnly do not all notify. Configure before RenderAsync or explicitly await renderer.UpdateAsync(control) after a host changes one. Declared fields do not promise every WinForms capability.

Many array getters return copies. Assign a new array instead of mutating returned Items/Rows/Nodes/Entries. Prefer meaningful operations: grid.SetCell (honors ReadOnly), checkedList.SetItemChecked, tree.SetExpanded, propertyGrid.SetEntryValue.

using var worker = new Forma.Core.Controls.BackgroundWorker();
worker.ProgressChanged += (_, percent) => progress.Value = percent;
worker.RunWorkerCompleted += (_, result) => { /* result.Error / result.Cancelled */ };
await worker.RunAsync(async (cancellation, report) => {
    for (int i = 0; i <= 100; i += 10) {
        cancellation.ThrowIfCancellationRequested();
        report.Report(i);
        await Task.Delay(50, cancellation);
    }
});

Here progress is a host-created ProgressBar. Use the UI synchronization context for UI/bridge updates, and dispose workers/timers on shutdown. Core Timer Tick occurs on a worker thread; Builder Preview explicitly marshals bridge work.

Events available today

The planned Events editor is future work. Scripts use DOM events through forma.on; C# subscribes to model events.

SourceCurrent JS eventCurrent C# result
ButtonclickClick
Text inputsinput; live valueTextChanged via SetText
Checkbox/radio/togglenative change; ToggleButton clickCheckedChanged
Choicesnative select change; ListView clicks bubbleSelectedIndexChanged
Numeric/date/time/colorinputValueChanged
Tabsheader click bubblesSelectedTab notification
DataGrid selectionrow-selection, event.detail.rowRowSelectionChanged
DataGrid cell editcell blurCellChanged, source row/column
TreeclicksSelectedNodeChanged / expansion notification
PaginationclicksPageChanged
PropertyGridnative input changePropertyValueChanged
Commandsitem clicksItemClicked
LinkLabelclickLinkClicked; host opens URL
Picker browsebutton clickBrowseRequested
TimertickTick
Dialog responsepopup is outside tray sourceClosed with Result
Toast dismissal/timeoutpopup is outside tray sourceClosed

Popup events do not bubble to their tray source. forma.on("Closed", ...) is not a C# event subscription. Only tick and row-selection are currently dispatched as the additional component events above. Declarations such as Button.MouseDown/MouseUp and TextBox.EnterPressed/KeyDown are not all wired through WebView2 yet; use the support table, not declarations alone.

Troubleshooting and performance

SymptomCheck
Control not foundExact Name/ID, case, uniqueness, generated suffix
Input read is staleRead value / component.element.value, not snapshot text
Unsupported propertyRuntime table; not every inspector field is a JS setter
Preview ignores a design editClose/reopen its cloned runtime
Font/color inspector ignoredRemove overriding Custom Properties CSS
Avatar shows initialsRe-select image; verify source/loadability/format
Flow/Table loses X/YManaged layout: reorder/change cell or use Panel/Card
Toast does not show on dropTrigger showToast / C# Show; open state is not persisted
Overlay does nothingActive, Enabled, valid Target; blank defaults to form
Picker has no dialog in own hostHandle BrowseRequested and assign SelectedPath
Array edit has no effectCopied getter: assign new array / use model edit method
Script failsInspect Preview title for the reported error

Optimization now merges repeated state requests, skips unchanged appearance, preserves modern-control DOM across unrelated edits, reuses inspector selection options, and avoids dirty-state serialization on simple selection. A regression test checks 100 controls over 20 unchanged passes without modern refreshes. That is a work-count check, not an end-to-end FPS measurement.

Large grids still render all visible rows; virtualization is pending. Design history still captures snapshots. Resize large source images appropriately, avoid unnecessarily short Timer intervals, debounce expensive input work, and use cleanup for timers/listeners. If lag remains, record control/row counts, image sizes and whether it occurs during dragging, property typing, save/open or Preview so the next profile targets the right path.

Search components, properties, and guides.

↑↓ navigate Enter open Esc close