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 events to actions; see §8 Events and actions.

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. Events and actions

A node's props may bind a DOM event to a named action. A binding is an object with type: "action", the action name, and optional params:

{
  "type": "div",
  "props": {
    "className": ["buy"],
    "onclick": {
      "type": "action",
      "action": "checkout",
      "params": {
        "sku": "joy-01"
      }
    }
  },
  "children": ["Buy now"]
}

The document names an action; the host supplies the handler. Nothing executable lives in the JSON, so a document stays serializable and safe to ship over the wire.

8.1 Event names

Four events 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.

8.2 Action binding

FieldTypeRequiredDescription
type"action"YesMarks the value as an action binding.
actionstringYesThe action name the host resolves to a handler.
paramsobjectNoStatic arguments passed to the handler with the event.

A renderer resolves each binding against a host-provided action map when it renders. How resolution and missing actions behave is renderer-specific; see the React renderer.

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/spec/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/spec package.

Changelog

  • 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