Joy DOM

Specification

The numbered, deep-linkable specification for Joy DOM nodes, styles, properties, and breakpoints.

The Joy DOM specification defines the JSON document format and the behavior every conforming renderer must implement. Sections are numbered so you can deep-link from a PR, ticket, or chat - for example, /docs/spec#521-flexdirection points at one property.

This document is friendly and example-driven. Every code sample is a complete, valid Joy DOM fragment.

1. Overview

1.1 What Joy DOM is

Joy DOM is a JSON document format. A document describes a tree of HTML-like nodes with CSS-like styles and media-query-like breakpoints. A renderer reads the JSON and maps it to a platform UI - React on the web, SwiftUI on Apple platforms, Compose on Android, with React Native planned.

Joy DOM targets cross-platform native rendering for a focused subset of HTML and CSS. Anything documented in this specification is supported by conforming renderers; anything not documented is not.

1.2 Goals and non-goals

Goals:

  • One document model rendered natively across platforms.
  • Strict alignment with HTML and CSS web standards for visual rendering and interactions.
  • Stable, machine-readable structure suitable for tooling and LLMs.

Non-goals:

  • Full HTML and CSS coverage. Joy DOM is intentionally a subset.
  • Custom syntax or features that deviate from web standards.

1.3 Document shape at a glance

A Joy DOM document has four top-level fields:

FieldTypePurpose
versionintegerThe spec version. Must be 1.
styleobjectShared styles keyed by selector.
breakpointsarrayResponsive overrides with media-query-like conditions.
layoutnodeThe root node of the document tree.

Minimal example:

{
  "version": 1,
  "style": {
    ".stack": {
      "display": "flex",
      "flexDirection": "column",
      "gap": {
        "value": 12,
        "unit": "px"
      }
    },
    "h1": {
      "display": "flex",
      "fontSize": {
        "value": 32,
        "unit": "px"
      }
    },
    "p": {
      "display": "flex"
    }
  },
  "breakpoints": [],
  "layout": {
    "type": "div",
    "props": {
      "className": ["stack"]
    },
    "children": [
      {
        "type": "h1",
        "children": ["Hello Joy DOM"]
      },
      {
        "type": "p",
        "children": ["Render structured web content across platforms."]
      }
    ]
  }
}

2. Document

2.1 version

Integer identifying the spec revision the document targets. The current version is 1. Renderers reject documents whose version they do not support. See §9 Versioning.

2.2 style

Object keyed by selector. See §4 Styles for selector syntax and resolution.

2.3 breakpoints

Array of breakpoint objects. See §6 Breakpoints. May be empty.

2.4 layout

The root node of the document tree. A node has a type (an HTML tag name or custom node name), optional props, and optional children (an array of nodes or primitive values - string, number, or null). See §3 Nodes.

3. Nodes

Joy DOM supports a focused subset of HTML node types. Each subsection below documents one node. Any node's props can also bind actions to events; see §8 Actions, events, and state.

3.1 div

Block container. Use for layout grouping.

{
  "type": "div",
  "props": {
    "className": ["card"]
  },
  "children": [
    {
      "type": "p",
      "children": ["Content"]
    }
  ]
}

3.2 span

Inline container. Use for inline grouping inside text.

3.3 p

Paragraph. Equivalent to HTML <p>.

3.4 img

Image. Required prop: src. Optional: alt. See §5.8 Media for sizing and fit properties.

{
  "type": "img",
  "props": {
    "src": "https://example.com/photo.png",
    "alt": "Photo description",
    "style": {
      "display": "flex",
      "width": {
        "value": 240,
        "unit": "px"
      },
      "height": {
        "value": 120,
        "unit": "px"
      },
      "objectFit": "cover"
    }
  }
}

3.5 h1 through h6

Headings. Same semantics as HTML <h1><h6>. Use fontSize and other typography properties from §5.6 to style them.

3.6 Custom nodes

Custom node names follow the WHATWG valid custom element name rules:

  1. Must contain a hyphen (-).
  2. Must be all-lowercase.
  3. Must not be a reserved name: annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph.

Each renderer registers a handler for custom node types. See §7 Custom components.

4. Styles

Styles let you share visual rules across a document and override them per node. Joy DOM supports type selectors, class selectors, id selectors, and inline node styles.

Use shared styles for reusable document structure. Use inline styles when one node needs a value that should not be reused.

4.1 Selectors

Shared styles live in the top-level style object, keyed by selector.

SelectorExampleMatches
TypepNodes with type: "p".
Class.titleNodes whose props.className includes title.
Id#heroNodes with props.id: "hero".

4.2 Resolution order

Joy DOM resolves styles in this order, later sources overriding earlier ones:

  1. Type selector.
  2. Class selectors, in the order listed in props.className.
  3. Id selector.
  4. Breakpoint styles when a breakpoint matches.
  5. Inline props.style.

Later values replace earlier values for the same property.

4.3 Inline styles

Inline styles are written on the node via props.style and take precedence over every shared selector.

{
  "type": "p",
  "props": {
    "className": ["muted"],
    "style": {
      "display": "flex",
      "color": "#111827"
    }
  },
  "children": ["Inline color wins over the shared class."]
}

4.4 Special rules

Conforming renderers enforce the following invariants:

  • Every node must explicitly declare display: "flex" or display: "none". Renderers throw on missing display.
  • The default box-sizing is border-box, not content-box.
  • Custom node names must be valid per §3.6.
  • Color values use hex strings. rgb() and rgba() are not supported.

5. Properties

This section documents every CSS property Joy DOM supports. Each entry follows the same shape: short description, values table, JSON example, and where useful a live preview rendered by @joy-dom/react. Anchors are stable: /docs/spec#521-flexdirection points at one property.

5.1 Layout and positioning

5.1.1 position

Positioning mode of the node.

FieldValue
Type'absolute' | 'relative'
Default'relative'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.1.2 display

Layout mode. Required on every node - see §4.4.

FieldValue
Type'flex' | 'none'
DefaultRequired.
InheritedNo

Set display: "flex" to render the node. Set display: "none" to hide it. Hiding inside a breakpoint override is the idiomatic way to toggle visibility responsively.

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.1.3 boxSizing

Box model behavior. The default is border-box, not content-box.

FieldValue
Type'border-box'
Default'border-box'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.1.4 zIndex

Stacking order for positioned nodes.

FieldValue
Typenumber
Default0
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.1.5 overflow

Overflow handling.

FieldValue
Type'visible' | 'hidden' | 'clip' | 'scroll' | 'auto'
Default'visible'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.1.6 top, left, bottom, right

Offset of an absolutely-positioned node from each side of its containing block.

FieldValue
TypeLength<'px'>
Defaultauto
InheritedNo

Used with position: "absolute". See §5.1.1 for a worked example.

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2 Flexbox

Every Joy DOM container is a flex container (via display: "flex"). The flex properties below describe its layout axis, alignment, growth, and spacing.

5.2.1 flexDirection

Main axis direction of a flex container.

FieldValue
Type'row' | 'column'
Default'row'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.2 flexGrow

Growth factor along the main axis.

FieldValue
Typenumber
Default0
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.3 flexShrink

Shrink factor along the main axis.

FieldValue
Typenumber
Default1
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.4 flexBasis

Initial size of an item before growth or shrink is applied.

FieldValue
TypeLength<'px' | '%'> | 'auto'
Default'auto'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.5 flexWrap

Whether items wrap onto multiple lines.

FieldValue
Type'nowrap' | 'wrap'
Default'nowrap'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.6 justifyContent

Alignment of items along the main axis.

FieldValue
Type'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly'
Default'flex-start'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.7 alignItems

Alignment of items along the cross axis.

FieldValue
Type'flex-start' | 'flex-end' | 'center' | 'stretch'
Default'stretch'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.8 alignSelf

Per-item cross-axis alignment override.

FieldValue
Type'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch'
Default'auto'
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.9 gap

Spacing between flex items along both axes.

FieldValue
TypeLength<'px'>
Default0px
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.10 rowGap

Spacing between flex items along the cross axis.

FieldValue
TypeLength<'px'>
Default0px
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.11 columnGap

Spacing between flex items along the main axis.

FieldValue
TypeLength<'px'>
Default0px
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.2.12 order

Item ordering inside a flex container. Use inside a breakpoint override to reorder responsively.

FieldValue
Typenumber
Default0
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.3 Sizing

5.3.1 width

FieldValue
TypeLength<'px' | '%'>
Defaultauto
InheritedNo

5.3.2 height

FieldValue
TypeLength<'px' | '%'>
Defaultauto
InheritedNo

5.3.3 minWidth

FieldValue
TypeLength<'px'>
Default0px
InheritedNo

5.3.4 maxWidth

FieldValue
TypeLength<'px'>
Defaultnone
InheritedNo

5.3.5 minHeight

FieldValue
TypeLength<'px'>
Default0px
InheritedNo

5.3.6 maxHeight

FieldValue
TypeLength<'px'>
Defaultnone
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.4 Spacing

5.4.1 padding

Inner spacing. Accepts a single length applied to all sides, or per-side lengths.

FieldValue
TypeLength<'px'> | { top, right, bottom, left } (each Length<'px'>)
Default0px
InheritedNo
{
  "padding": {
    "top": {
      "value": 12,
      "unit": "px"
    },
    "right": {
      "value": 16,
      "unit": "px"
    },
    "bottom": {
      "value": 12,
      "unit": "px"
    },
    "left": {
      "value": 16,
      "unit": "px"
    }
  }
}

5.4.2 margin

Outer spacing. Same shape as padding.

FieldValue
TypeLength<'px'> | { top, right, bottom, left } (each Length<'px'>)
Default0px
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.5 Borders

5.5.1 borderWidth

FieldValue
TypeLength<'px'>
Default0px
InheritedNo

5.5.2 borderColor

Hex string. rgb() / rgba() are not supported.

FieldValue
Typestring
Default'transparent'
InheritedNo

5.5.3 borderStyle

FieldValue
Type'solid' | 'none'
Default'none'
InheritedNo

5.5.4 borderRadius

Corner rounding. Accepts a single length applied to all corners, or per-corner lengths.

FieldValue
TypeLength<'px'> | { topLeft, topRight, bottomRight, bottomLeft } (each Length<'px'>)
Default0px
InheritedNo

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.6 Typography

Typography properties apply to text nodes: p, h1h6, and span. Most inherit from ancestor nodes.

5.6.1 fontFamily

FieldValue
Typestring
Defaultsystem default
InheritedYes

5.6.2 fontSize

FieldValue
TypeLength<'px'>
Default16px
InheritedYes

5.6.3 fontWeight

FieldValue
Type'normal' | 'bold' | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
Default'normal' (400)
InheritedYes

5.6.4 fontStyle

FieldValue
Type'normal' | 'italic'
Default'normal'
InheritedYes

5.6.5 color

Hex string. rgb() / rgba() are not supported.

FieldValue
Typestring
Default'#000000'
InheritedYes

5.6.6 textDecoration

FieldValue
Type'none' | 'underline' | 'line-through'
Default'none'
InheritedNo

5.6.7 textAlign

FieldValue
Type'left' | 'center' | 'right'
Default'left'
InheritedYes

5.6.8 textTransform

FieldValue
Type'none' | 'uppercase' | 'lowercase'
Default'none'
InheritedYes

5.6.9 lineHeight

Multiplier of the font size.

FieldValue
Typenumber
Default1.2
InheritedYes

5.6.10 letterSpacing

FieldValue
TypeLength<'px'>
Default0px
InheritedYes

5.6.11 textOverflow

Behavior when text overflows its container.

FieldValue
Type'clip' | 'ellipsis'
Default'clip'
InheritedNo

text-overflow: "ellipsis" requires all of:

  • overflow: "hidden" on the same node.
  • whiteSpace: "nowrap" on the same node.
  • A constrained width (from the parent).
  • Text rendered directly inside that node (for example, inside a p).

5.6.12 whiteSpace

FieldValue
Type'normal' | 'nowrap'
Default'normal'
InheritedYes

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.7 Backgrounds

5.7.1 backgroundColor

Hex string. rgb() / rgba() are not supported.

FieldValue
Typestring
Default'transparent'
InheritedNo

5.7.2 opacity

FieldValue
Typenumber (0–1)
Default1
InheritedNo

5.7.3 Background images

The background-image CSS property is not yet supported. Use an img node as a positioned background layer instead:

  1. Set position: "relative" on the wrapper.
  2. Add an img child positioned with position: "absolute" and pinned to the wrapper's edges.
  3. Give the foreground content a higher zIndex than the image.
{
  "type": "div",
  "props": {
    "style": {
      "display": "flex",
      "position": "relative",
      "overflow": "hidden",
      "height": {
        "value": 320,
        "unit": "px"
      }
    }
  },
  "children": [
    {
      "type": "img",
      "props": {
        "src": "https://example.com/photo.png",
        "style": {
          "display": "flex",
          "position": "absolute",
          "top": {
            "value": 0,
            "unit": "px"
          },
          "left": {
            "value": 0,
            "unit": "px"
          },
          "right": {
            "value": 0,
            "unit": "px"
          },
          "bottom": {
            "value": 0,
            "unit": "px"
          },
          "width": {
            "value": 100,
            "unit": "%"
          },
          "height": {
            "value": 100,
            "unit": "%"
          },
          "objectFit": "cover",
          "zIndex": 0
        }
      }
    },
    {
      "type": "div",
      "props": {
        "style": {
          "display": "flex",
          "position": "relative",
          "zIndex": 1
        }
      },
      "children": [
        {
          "type": "p",
          "children": ["Foreground content"]
        }
      ]
    }
  ]
}

Use breakpoint overrides to change objectFit, objectPosition, wrapper size, or foreground spacing per width or orientation.

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

5.8 Media

img nodes accept the properties below to control how the source asset scales and positions within the node's box.

5.8.1 objectFit

FieldValue
Type'fill' | 'contain' | 'cover' | 'none'
Default'fill'
InheritedNo

5.8.2 objectPosition

Anchor point inside the node's box.

FieldValue
Type{ horizontal: 'left' | 'center' | 'right', vertical: 'top' | 'center' | 'bottom' }
Default{ horizontal: 'center', vertical: 'center' }
InheritedYes

Cross-renderer parity: React ✅ · Swift ⚠️ pre-release · Kotlin ⚠️ pre-release

6. Breakpoints

Breakpoints apply node and style overrides when media-query-like conditions match. Use them for layout changes, visibility, order, print output, and device-specific typography.

Joy DOM keeps one primary node tree. Breakpoints change styles and node props inside that tree - they do not introduce a parallel tree.

6.1 Breakpoint object

A breakpoint has three required fields:

FieldDescription
conditionsArray of conditions that activate the breakpoint.
nodesNode prop overrides keyed by node id.
styleShared style overrides keyed by selector.

6.2 Conditions

Joy DOM supports four condition types and three logical operators.

ConditionValues
Media typeprint
Width featurewidth with >, <, >=, or <= in px
Orientation featureportrait or landscape
Logical operatorsand, or, not

Width:

{
  "type": "feature",
  "name": "width",
  "operator": "<",
  "value": 768,
  "unit": "px"
}

CSS equivalent: @media (width < 768px) { ... }

Print:

{
  "type": "type",
  "value": "print"
}

CSS equivalent: @media print { ... }

Width and orientation, joined by and:

{
  "op": "and",
  "conditions": [
    {
      "type": "feature",
      "name": "width",
      "operator": ">=",
      "value": 768,
      "unit": "px"
    },
    {
      "type": "feature",
      "name": "orientation",
      "value": "landscape"
    }
  ]
}

CSS equivalent: @media (width >= 768px) and (orientation: landscape) { ... }

Negation with not:

{
  "op": "not",
  "condition": {
    "type": "type",
    "value": "print"
  }
}

CSS equivalent: @media not print { ... }

6.3 Matching algorithm

When more than one breakpoint matches, Joy DOM chooses exactly one:

  1. The breakpoint with the more specific condition set wins.
  2. If specificity is tied, the later breakpoint in the array wins.

Multiple matching breakpoints are not merged.

6.4 Common patterns

Change order. Use order inside a breakpoint to reorder siblings:

{
  "conditions": [
    {
      "type": "feature",
      "name": "width",
      "operator": "<",
      "value": 768,
      "unit": "px"
    }
  ],
  "nodes": {
    "sidebar": {
      "style": {
        "order": 2
      }
    }
  },
  "style": {}
}

Remove order from the override to fall back to the primary tree order.

Toggle visibility. Use display: "none" on the primary node or inside a breakpoint override:

{
  "conditions": [
    {
      "type": "feature",
      "name": "width",
      "operator": "<",
      "value": 768,
      "unit": "px"
    }
  ],
  "nodes": {
    "desktop-nav": {
      "style": {
        "display": "none"
      }
    }
  },
  "style": {}
}

Override props. Breakpoint node props merge with primary node props - only the listed fields change:

{
  "conditions": [
    {
      "type": "feature",
      "name": "width",
      "operator": "<",
      "value": 768,
      "unit": "px"
    }
  ],
  "nodes": {
    "hero-title": {
      "style": {
        "fontSize": {
          "value": 40,
          "unit": "px"
        }
      }
    }
  },
  "style": {}
}

7. Custom components

A custom node is a node with a type that follows the rules in §3.6 Custom nodes. Each renderer registers a handler keyed by the custom name; when the renderer encounters a node of that type, it delegates rendering to the registered handler.

{
  "type": "my-button",
  "props": {
    "id": "primary-action"
  },
  "children": ["Continue"]
}

The document owns the node type and props. The renderer owns the component implementation.

7.1 Component props

Renderers pass these props to a custom component:

PropDescription
childrenRendered child nodes and primitive values.
nodeThe original Joy DOM node.
idThe resolved node id.
classNameRenderer-generated classes plus document classes.
styleResolved inline style for the platform renderer.

If a document uses a custom node type without a registered component, the renderer throws an error.

7.2 Registration

Registration is renderer-specific. See:

8. Actions, events, and state

A document carries state (plain JSON it owns), reads it through directives, and changes it through actions declared in a root-level handlers region and referenced from node event properties:

interaction → action runs → state changes → UI updates → host notified

Three concepts, kept separate:

  • State — the source of truth. Plain JSON. Changed only by mutation actions.
  • Directives$-keyed reads and computations. Read-only; they never change state.
  • Actionstype-tagged objects in the handlers region. The only things that change state or reach the host.

Nothing executable lives in the JSON, so a document stays serializable and safe to ship over the wire.

8.1 Event properties and handler references

Four event properties bind, each matching its HTML counterpart:

PropertyFires on
onclickA click or tap on the node.
onfocusThe node gaining focus.
onblurThe node losing focus.
onchangeA committed value change on an input node.

An event property holds a reference: an event object whose name keys an entry in the root handlers region.

{
  "handlers": {
    "goToCheckout": { "type": "set", "path": "/route", "to": "checkout" }
  },
  "layout": {
    "type": "div",
    "props": { "onclick": { "type": "event", "name": "goToCheckout" } }
  }
}

When the event fires, the named handler runs. A name that matches no handler dispatches to the host as a plain event (see §8.4), so notifying the host needs no handler entry.

A reference's params carry per-node data into the handler: they are resolved with the rest of the template (so a $for item's bindings work), materialized against the firing event's payload, and merged into the payload the handler's $event reads.

8.2 The action union

Each handlers entry holds a single action, one of three kinds:

KindtypePurpose
Mutationset, merge, insert, update, upsert, remove, move, toggle, incrementChange state.
EventeventReach the host.
SequencesequenceRun several actions in order.
"handlers": {
  "goToCheckout": { "type": "set", "path": "/route", "to": "checkout" },
  "checkout": { "type": "event", "name": "checkout" },
  "saveAndClose": { "type": "sequence", "actions": [  ] }
}

One handler, one action. When an interaction needs to do more than one thing, that action is a sequence.

8.3 Mutations

A mutation names a type (the verb), a path into state (a JSON Pointer, always static), and whatever the verb needs. Examples below run against this state:

{
  "prefs": { "theme": "light", "darkMode": false },
  "tags": ["red", "green"],
  "cart": [
    { "id": "TEE-01", "qty": 1 },
    { "id": "CAP-01", "qty": 2 }
  ]
}
VerbDoes
setWrite to at path. Replaces what's there; creates missing object parents. On an array, a positional path replaces, the - token appends.
mergeShallow-merge value into the object at path. null sets null; it does not delete.
insertAdd value (or several values) to the array at path. Appends, or inserts at index.
updateWrite set fields onto the array elements that match.
upsertInsert value into the array, or apply onConflict to the element with the same key. Without onConflict, the match is replaced.
removeDelete the value at path, or every matching array element. Never errors on absence.
moveReorder the array at path, from one position to another.
toggleFlip the boolean at path. A missing value becomes true.
incrementAdd by (default 1, may be negative) to the number at path. A missing value counts from 0.
{ "type": "set",       "path": "/prefs/theme", "to": "dark" }
{ "type": "merge",     "path": "/prefs", "value": { "theme": "dark" } }
{ "type": "insert",    "path": "/tags", "index": 0, "value": "blue" }
{ "type": "update",    "path": "/cart", "key": "id", "equals": "TEE-01", "set": { "saved": true } }
{ "type": "upsert",    "path": "/cart", "key": "id", "value": { "id": "CAP-01", "qty": 1 }, "onConflict": { "qty": 3 } }
{ "type": "remove",    "path": "/cart", "key": "id", "equals": "TEE-01" }
{ "type": "move",      "path": "/cart", "from": 2, "to": 0 }
{ "type": "toggle",    "path": "/prefs/darkMode" }
{ "type": "increment", "path": "/cart", "key": "id", "equals": "CAP-01", "field": "/qty", "by": 1 }

Addressing array elements. path always points at the container. Verbs that operate on one element accept a selector:

SelectorMatches
indexThe element at that position. Out of range matches nothing.
key + equalsEvery object element whose key field deep-equals equals.
…plus fieldA JSON Pointer inside each matched element (set, merge, toggle, increment).

Dynamic values. Every value position may hold a directive: to, value, equals, index, by, set fields, params. path may not. Handlers are global, so $get reads in a handler resolve against state; per-node data (a $for item's fields, for instance) travels through the reference's params. { "$event": "/value" } resolves later, when the event fires (see §8.6).

8.4 Events

An event action reaches outside the document — the host navigates, calls an API, logs, or anything else:

{
  "type": "event",
  "name": "checkout",
  "params": { "cartId": { "$get": "/cart/id" } }
}
FieldTypeRequiredDescription
type"event"YesMarks the action as an event.
namestringYesThe signal the host receives.
paramsobjectNoValues delivered with the event. Literals or directive reads.

The host receives events through a single onEvent interface, with a handle to respond through. Three rules keep the contract clean:

  • The host is notified after the interaction's state changes have applied — it always sees settled state.
  • The host responds by dispatching actions through its handle.
  • Anything the host dispatches is a new interaction, queued after the current one.

Interactions run one at a time, first in, first out. An event raised while one is being handled waits its turn, so the order is always predictable.

8.5 Sequences

A sequence runs several actions from one event property, in order:

{
  "type": "sequence",
  "actions": [
    { "type": "toggle", "path": "/prefs/darkMode" },
    { "type": "event", "name": "themeToggled" }
  ]
}

Actions run one at a time, exactly where they sit in the list — an event between two mutations fires between them. Events reaching the host can't be taken back, so put a mutation before the event that should reflect it. A single action and a one-item sequence mean the same thing.

8.6 State

State is the document's own data, declared in the template's state region. The UI is a view of state; mutation actions are the only writers.

State is read through directives ($get, $if, $for — see the template documentation). One directive is exclusive to actions: $event reads the interaction's payload, the data the firing event carried:

"handlers": {
  "saveEmail": { "type": "set", "path": "/draft/email", "to": { "$event": "/value" } }
}

The payload is normalized per event so documents behave the same on every platform:

EventPayload
onchange{ "value": string | number | boolean }
onclick, onfocus, onblur{}

A pointer that misses reads as null. A reference's params merge into this payload before the handler runs, so a handler reads { "$event": "/id" } the same way whether id came from the event or from the reference. $event is valid only inside the handlers region and event-property references; validate reports it anywhere else.

8.7 Errors

Mutations are absence-tolerant and shape-strict:

  • Absence degrades. Missing parents are created (set, merge, toggle, increment), missing targets are no-ops (remove, move, update), missing numbers count from zero.
  • Shape mismatches stop the interaction. Incrementing a string, toggling an object, inserting into a non-array — each raises a typed error naming the verb, the path, and the action's position. Actions already applied stay applied; the actions after it don't run.

An error is a bug signal, not a control-flow tool: the fix is the document (or the host write that broke the state's shape), not a rollback. Renderers never crash on one — they report it through an error callback and keep rendering.

9. Versioning

The version field at the top of every document declares the spec revision the document targets. The current version is 1. Renderers reject documents whose version they do not support.

Versioning is semantic at the JSON Schema level: a major bump can break document compatibility; minor and patch bumps only add or refine properties.

10. JSON Schema

A machine-readable JSON Schema for the full document is generated from the same definitions that back this specification, so the two cannot drift. It lives at js/core/src/schema.json in the repository. Point editors, validation CLIs, or non-JS tooling at that file to validate documents against the format.

JavaScript and TypeScript projects can import the equivalent runtime validators and inferred types from the @joy-dom/core package.

Changelog

  • 2026-07-23. Rewrote §8 as the full action model: mutation verbs with index/key+equals/field addressing, sequences, document-owned state, the $event payload read, and the absence-tolerant/shape-strict error contract.
  • 2026-06-13. Reshaped the §8 event binding to { "type": "event", "name" } (was { "type": "action", "action" }).
  • 2026-06-09. Added §10 JSON Schema: a pointer to the generated schema file for editors and non-JS tooling.
  • 2026-06-05. Added §8 Events and actions: a node's props can bind onclick, onfocus, onblur, and onchange to named actions.
  • 2026-05-22. Spec restructured into a single numbered document. Previous pages (specification, styles, css, breakpoints, text-styles, background-images, components) folded into this page.

On this page