Element
A node in a page or component tree — Primitive or Component kind, with props, styles, and optional repeat.
An Element is one node in a page or component tree. Kind + props (attributes) + styles, optional repeat and renderIf. It belongs to exactly one container — a page or a component, never both, never the app directly.
The canvas is this tree. There is no parallel source of truth. A page tree is rooted at body.

Kind
| Kind | Pass to tools | Meaning |
|---|---|---|
| Primitive | { _tag: "Primitive", value: "section" } | A registry widget: HTML, coss, image, … |
| Component | { _tag: "Component", value: "<componentId>" } | An instance of a user component. list_components first. |
Known primitive names must use _tag: "Primitive". You cannot pass "Button" as a Component.
Fields
| Name | Type | Default | Description |
|---|---|---|---|
props | attributes | — | Kind-specific and custom values: children, href, className, instance fields, event prop slots. A slot is a literal or a {{…}} expression. Event slots store one function binding (functionId + instanceParams). |
styles | style state | — | Responsive style. MCP writes these with style_*, not add_element. On insert, bulk Tailwind goes in className. |
repeatable | boolean | false | When true, the node repeats once per item of repeatSource. Inside the subtree: {{repeat.item}}, {{repeat.index}}, {{repeat.key}}. You author one node; render expands copies. |
repeatSource | {{…}} | — | A complete expression that resolves to an array. |
renderIf | {{…}} | always | Boolean expression. Falsy → this node and its subtree are omitted. |
parentId | string | — | Tree parent. You almost never set this raw — use parentElementId + targetPosition on add/move. |
siblingPosition | number | — | Order among siblings. Same: set via targetPosition. |
The display name in outlines and search is aria-label → component name → kind value.
Tree mutations take a container:
| Container | Pass |
|---|---|
| Page | { _tag: "Page", id: pageId } |
| Component | { _tag: "Component", id: componentId } |
How it relates
- Page / component — the two containers.
add_elementwith noparentElementIdappends under that container's root (bodyon a page). - Primitive vs Component kind — a primitive is a registry widget (
div,Button,Dialog). A Component kind is an instance of a user component; its props are that instance's values. - Props / Function — settable keys are prop slots. Event slots store one function binding; sequences live on a store Action.
- App — theme tokens and kind styles paint every element of a kind. Per-element
style_*is the last layer.
MCP tools
Read first
| Tool | What you use it for |
|---|---|
list_element_types | Index by category (layout, form, overlay, …). Call once per group; pass the returned kind to add_element. Do not pass category to add. |
inspect_element_type | Required before any composite primitive. Canonical composition + pitfalls. |
list_examples | Copy-paste patterns by kind / intent when the inspect doc is not enough. |
load_page_outline / load_component_outline | Whole tree. Heavy — prefer find when you can. |
find_element_in_page / find_element_in_component | Label / kind / text → ids. |
inspect_element | One node's props, styles, renderIf, repeat. |
Write
| Tool | What you use it for |
|---|---|
add_root_element | First node of an empty component tree. Pages already have body. Root kind = Primitive only. |
add_element | One child. parentElementId omitted → under the root. |
add_elements | A subtree in one persist. Parent command before child; mint elementId on parents so later commands can point at them. Prefer this over N add_element calls. |
update_props | Prop slots: children, href, src, instance fields, expressions, function bindings. Set/unset commands — not a full replace. |
update_element | Node fields: kind, repeatable, repeatSource, renderIf. |
style_* | Incremental, breakpoint-aware style (style_layout, style_typography, style_spacing_padding, …). Discover keys with style_schema. |
move_element | Reparent / reorder. Same targetPosition rules. Cannot move the root or into itself. |
remove_element | Delete nodes. mode: "withReparent" unwraps a wrapper and keeps children. This is not delete_component. |
duplicate_element_tree | Deep copy as the next sibling. New ids. Cannot duplicate the root. |
bind_prop | Inside a component: {{props.<key>}} onto a target prop. |
add_element
| Argument | Required | Description |
|---|---|---|
container | yes | Page or component, as in the table above. |
kind | yes | Primitive or Component, as in Kind. |
parentElementId | no | Omit → under the root (body on a page). |
targetPosition | no | Omit to append. 0 = first. |
props | no | Array of { key, value } with string / number / boolean only. Use className, never class. |
Objects, expressions that are not a whole {{…}} string, and function bindings go on update_props after create. Set aria-label to 1–3 words of purpose (Hero, Pricing, Close) and put Tailwind in className.
update_element (repeat)
| Command | Value |
|---|---|
set repeatable | true |
set repeatSource | "{{props.events}}" (a whole {{…}}) |
Event prop
One function binding per slot — sequences live on store actions, not on the element. update_props set on onClick (or onChange, …) with functionId + instanceParams. See Function and Action.
Gotchas
aria-labelis required on add, and it is a real DOM attribute. 1–3 words of purpose:"Hero","Pricing","Close". Not"div","section wrapper", or a visual description. The tree andfind_element_*use it as the label. A gratuitous label on a node that already has visible text overrides that text in the accessibility tree — still set a purpose name; do not use it as a fake editor tag.- Omit
targetPositionto append.targetPosition: Ninserts at index N (the current N and everything after shift down).0= first. Passingchildren.lengthis the off-by-one: with 6 children (0..5),5lands before the last. To make it last, omit the field. Same rule onmove_element. inspect_element_typebefore composites. Blocking for Radio / RadioGroup, SelectItem, MenuItem, Dialog (Trigger / Popup / Header / Title / Description / Panel / Footer / Close), Field + FieldLabel, InputGroup, AccordionTrigger, … Skip only unambiguous leaves (div,span,p,h1–h6,a,img,hr,br). Wrong wrappers fail silently (stacked radios, popups outside their trigger).- Three write tools, three jobs.
update_props= prop slots (includingchildrenand function bindings).update_element=kind/ repeat /renderIf.style_*= style. Commands aresetorunseton a key — last write wins; never send the whole object. - Place it once.
parentElementId+targetPositionon insert. Do not add thenmove_element. For a whole subtree,add_elements(parent before child). repeatSourceis one whole binding.{{props.events}}or{{stores.x.items}}, not a fragment.