Skip to content

Elements

Studio writes this format for you. Every element you place on the canvas and edit in the Properties panel is stored as one of these objects, and this page documents what lands in the file.

An element definition is a JSON object describing one DOM element. Its keys are the element's real DOM property names, set directly on the created element with no translation layer in between. If a property exists on the DOM element, you can set it here.

{
  "tagName": "div",
  "id": "my-element",
  "className": "container active",
  "hidden": false,
  "tabIndex": 0,
  "textContent": "Hello World"
}

DOM properties

Property names follow the DOM, not HTML source: className (not class ), textContent (not inner text), tabIndex (not tabindex ). Any string-valued property may contain a template for a reactive value (see Reactivity ), or be an object with $ref bound to state (see References ):

{
  "tagName": "div",
  "textContent": "undefined items remaining",
  "hidden": 
}

Two properties are protected : id and tagName identify the element and may not be set via $ref bindings.

Custom attributes

Anything that is not a standard DOM property ( data-* , aria-* , slot ) goes in the attributes object, written exactly as it appears in HTML:

{
  "tagName": "div",
  "attributes": {
    "data-component": "my-widget",
    "aria-label": "Interactive counter",
    "slot": "header"
  }
}

Attribute values may also be reactive templates ( "aria-label": "undefined unread messages" ).

Boolean values

An attribute value of true or false , written directly or resolved from a template, is spelled the way the attribute itself is read. HTML has two rules here, and Jx picks the right one from the attribute name, so conditional markup is just a boolean:

{
  "tagName": "details",
  "attributes": { "open": "undefined" }
}

Presence attributes are read by presence alone: open , disabled , checked , hidden , required , selected and the rest of the HTML boolean family. true writes the bare name, false removes the attribute entirely:

<details open></details>
<!-- state.expanded === false -->
<details></details>
Warning

Never write "open": "false" as a string. HTML counts any value as present, "false" included, so <details open="false"> is an open <details> . This is the reason a boolean is not stringified.

Enumerated attributes carry the word in their text and treat an empty value as unset. Every aria-* attribute is one, along with contenteditable , draggable and spellcheck . A boolean writes the word for these, in both directions:

{ "attributes": { "aria-expanded": "undefined" } }
<div aria-expanded="true"></div>
<!-- state.open === false -->
<div aria-expanded="false"></div>

That distinction matters for accessibility: a bare aria-hidden is not hidden, and an omitted contenteditable means "inherit from the parent" rather than false . Writing either as a presence attribute would silently invert it.

A string is never reinterpreted in either family. "aria-current": "false" stays exactly that, which is how you write the not-the-current-page marker beside "aria-current": "page" .

The compiled page and the live runtime apply the same rule, so a prerendered element does not change meaning when it hydrates.

Children

children is an array of element definitions, rendered in order:

{
  "tagName": "div",
  "children": [
    { "tagName": "h1", "textContent": "Title" },
    { "tagName": "p", "textContent": "Content" }
  ]
}

A children array may freely mix element objects, bare text nodes, and repeaters ( $prototype: "Array" members) or switches .

Text nodes

Bare strings and numbers are valid children items. They produce DOM Text nodes directly, with no wrapper element. This is how text with inline markup is written:

{
  "tagName": "p",
  "children": ["Hello ", { "tagName": "strong", "textContent": "world" }, "!"]
}

That is the HTML <p>Hello <strong>world</strong>!</p> . Template strings in text nodes are reactive: { "children": ["Welcome, undefined!"] } .

Slots

Custom elements use the standard HTML slot mechanism for content composition. A component's template places <slot> elements; content the instance provides is distributed to them by name :

{
  "tagName": "card-component",
  "children": [
    {
      "tagName": "header",
      "children": [{ "tagName": "slot", "attributes": { "name": "header" } }]
    },
    { "tagName": "main", "children": [{ "tagName": "slot" }] }
  ]
}

A slot's own children act as fallback content, kept when the instance provides nothing for it.

Annotations

Any element may carry $title and $description , developer-facing labels that never reach the DOM:

{
  "tagName": "section",
  "$title": "Hero Section",
  "$description": "Primary landing area with headline and call-to-action",
  "children": []
}

Studio's Outline panel shows $title as the element's display name.

How it works

The runtime creates the element with document.createElement(tagName) , assigns each listed property directly on the element object, and writes attributes entries with setAttribute . Properties whose values are templates or $ref bindings are wrapped in reactive effects, so the DOM updates whenever the underlying state changes. Slot distribution is manual light-DOM distribution: host children are captured before the template renders, then moved to matching <slot> elements by name .

Rules

  • tagName is required on every element definition.

  • id and tagName are protected, and never settable via $ref .

  • Standard DOM properties go at the top level; everything else ( data-* , aria-* , slot ) goes in attributes .

  • Bare strings and numbers in children become text nodes; when all children are bare strings with no element siblings, prefer textContent instead.

  • $title and $description are plain strings: not reactive, not $ref -resolvable, never applied to the DOM.

  • Styling does not use DOM properties. The style object has its own grammar, covered in Styling .