Components API
Reusable building blocks with typed inputs, computed outputs, and encapsulated logic.
Try it
title: "Component instance"
description: "Two button instances with different inputs"
nodes:
- type: RECT, name: "Btn1Bg", x: 120, y: 200, w: 120, h: 40, fill: "#3498DB", cornerRadius: 6
- type: RECT, name: "Btn2Bg", x: 280, y: 200, w: 120, h: 40, fill: "#E74C3C", cornerRadius: 6
events:
- node: "Btn1Bg", event: "click", code: "this.opacity = this.opacity === 1 ? 0.5 : 1"
- node: "Btn2Bg", event: "click", code: "this.opacity = this.opacity === 1 ? 0.5 : 1"
tools_visible: [select, play]
ui_visible: [properties, formulas, layers]Component definition
ComponentDef {
id: string // unique identifier
name: string // display name ("Button", "Card")
inputs: ComponentProperty[] // parameters from outside
outputs: ComponentProperty[] // computed values exposed outside
nodes: VNode[] // internal structure
formulas: FormulaMap // internal reactive bindings
}
ComponentProperty {
name: string // property name
type: "number" | "string" | "boolean" | "color" | "point"
defaultValue: any // value when not overridden
description?: string // tooltip text
}Inputs
Parameters that control the component from outside. Set by the parent scene or wired with formulas.
Component "ProgressBar" Inputs: value: number = 0 ← current progress (0-100) color: Color = "#3498DB" ← bar color height: number = 8 ← bar height
Inside the component, reference inputs with $input:
Bar.width = $input.value / 100 * Track.widthBar.fills[0].color = $input.colorBar.height = $input.height
Outputs
Computed values available to the parent. Read-only from outside.
Outputs: isComplete: boolean ← true when value >= 100 percentage: number ← value / 100
Parent formulas:
DoneText.visible = ProgressBar.isCompleteLabel.text = ProgressBar.percentage
Instances
Place a component on the canvas → creates an instance.
- Internal nodes are cloned from the definition
- Each instance has independent state
- Change the definition → all instances update
- Override an input on one instance → only that instance changes
ComponentInstance {
defId: string // which definition
inputValues: { [name: string]: any | string } // static values or formulas
}Creating components
- Select a group of nodes
- "Create Component" action
- Define which properties become inputs
- Define which computed values become outputs
- Internal formulas reference
$input.propertyName
Non-visual components
Components that don't draw to the canvas. Logic-only entities with reactive interfaces.
Timer
Inputs: interval: number = 1000, active: boolean = falseOutputs: elapsed: number, tick: event
API Connection
Inputs: url: string, method: string = "GET", body: objectOutputs: data: any, loading: boolean, error: stringActions: fetch()
State
Inputs: initialValue: anyOutputs: value: anyActions: set(val), reset()
Non-visual components appear in the scene tree with a distinct icon. Their outputs wire to visual components via formulas:
Spinner.visible = api.loadingDataTable.data = api.data
Slots
Typed inputs that accept VNodes (not values) from the parent scene.
ComponentSlot {
name: string // "leftFootTarget", "lookAtTarget"
type: string // expected node type or "any"
placeholder?: VNode // debug-time stand-in
}During authoring: each slot creates a placeholder node for testing. At runtime: placeholders replaced by actual scene nodes.
Formula integration:
EyeRotation = atan2($lookAtTarget.y - this.y, $lookAtTarget.x - this.x)
Properties panel shows drop zones for slots — drag a node from the tree to bind it.
Custom editors
Build custom inspector panels for your component types.
Editor for "DataChart"├── Dropdown → $target.input.chartType├── ColorInput → $target.input.primaryColor├── Slider → $target.input.barWidth└── Button → resetDefaults($target)
When a user selects an instance, the custom editor replaces the default properties panel. Custom editors are themselves components.
Editor widgets
| Widget | Purpose |
|---|---|
| PropertyField | Auto-generates input for a property type |
| PropertyGroup | Collapsible section header |
| NodePicker | Select a node from document |
| FormulaInput | Formula field with syntax highlighting |
| PreviewCanvas | Mini canvas preview |
| ActionButton | Triggers a script/action |
Nesting
Components contain other component instances. Inputs wire through:
Page component├── Input: theme.primaryColor└── Button instance └── color = $input.theme.primaryColor
Change the page theme → all buttons update.
Export
| Component type | React export |
|---|---|
| Visual | React component with props |
| Non-visual | Custom hook |
| Slots | Context or ref bindings |
| Custom editor | (editor-only, not exported) |
// Visual component
function Button({ label, color, disabled }) {
return <button style={{ backgroundColor: color }}>{label}</button>
}
// Non-visual component
function useTimer(interval, active) {
const [elapsed, setElapsed] = useState(0)
useEffect(() => { ... }, [interval, active])
return { elapsed }
}Serialization
{
"type": "COMPONENT_INSTANCE",
"defId": "button-v1",
"inputValues": {
"label": "Save",
"color": "#2ECC71",
"disabled": false
}
}Definitions serialize separately from instances. On load, instances resolve their definition by ID and clone internal nodes.
→ Nodes → Properties → Formulas API → Events API