Joy DOM

Overview

Render Joy DOM documents with Compose Multiplatform.

The Kotlin renderer (joy-dom-kotlin) decodes a Joy DOM JSON document and renders it as Compose UI. It's a Compose Multiplatform library: the modules build for JVM (desktop) and iOS today, with Android on the roadmap. The public package root is com.j0y.joy.dom.

Status: pre-release

The renderer is split into Gradle modules under kotlin/ and isn't published to Maven yet. Use it locally by including the modules in your build.

Render a document

JoyDomCompose is the entry point. It probes the available width, resolves the cascade, and walks the tree inline:

import androidx.compose.runtime.Composable
import com.j0y.joy.dom.render.compose.JoyDomCompose
import com.j0y.joy.dom.serialization.Spec

@Composable
fun InvoiceScreen(spec: Spec) {
    JoyDomCompose(spec = spec)
}

JoyDomCompose measures itself with BoxWithConstraints, derives a viewport from the constraints and density, resolves spec.style and breakpoints, then renders each node: flex containers as Compose layout, text nodes as Text, and img nodes through a painter you supply. A second overload takes a pre-resolved ResolvedSpec when you want to control the viewport yourself.

Custom components

Pass a JoyDomComponents map keyed by the kebab-case node type. Each value is a composable that receives the resolved node, with children, style, and props already resolved on it:

import androidx.compose.runtime.Composable
import com.j0y.joy.dom.render.compose.JoyDomComponent
import com.j0y.joy.dom.render.compose.JoyDomComponents
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.intOrNull

val stars: JoyDomComponent = { node ->
    val value = (node.propsExtras["value"] as? JsonPrimitive)?.intOrNull ?: 0
    Row { repeat(5) { i -> Text(if (i < value) "★" else "☆") } }
}

val components: JoyDomComponents = mapOf("rating-stars" to stars)

JoyDomCompose(spec = spec, components = components)

Read props off node.propsExtras (each value is a JsonElement), children off node.children, and resolved style off node.style. Built-in types (div, span, p, h1h6, img) never reach this map. For img loading, pass a painterFactory: @Composable (src: String) -> Painter?.

Events and actions

Build a handler config with the events { } DSL and pass it as events. Handlers are keyed by the binding's action name; params carries the binding's static arguments:

import com.j0y.joy.dom.render.compose.events
import com.j0y.joy.dom.render.compose.MissingAction

val actions = events {
    missingAction = MissingAction.Ignore   // or .Throw (the default)
    action("checkout") {
        val sku = params?.getStringOrNull("sku")
        // ...
    }
}

JoyDomCompose(spec = spec, events = actions)

Loading documents

Decode with the pre-configured JoyDomJson instance:

import com.j0y.joy.dom.serialization.JoyDomJson
import com.j0y.joy.dom.serialization.Spec

val spec = JoyDomJson.decodeFromString(Spec.serializer(), jsonString)

JoyDomJson matches the format @joy-dom/spec defines, so documents are portable across renderers.

Module map

ModulePurpose
:dom-serializationSpec data classes + kotlinx.serialization (the configured JoyDomJson).
:dom-layoutStyle cascade and resolution: resolve(spec, viewport)ResolvedSpec.
:dom-renderPlatform-agnostic Renderer<T, S> visitor + HeadlessRenderer.
:dom-render-composeThe Compose binding (JoyDomCompose).
:dom-dslOptional Kotlin DSL for building Spec objects programmatically.
:dom-converter:htmlHTML ↔ Spec conversion.

Local development

cd kotlin
./gradlew build

Sample apps live under kotlin/sample-desktop (a Compose Desktop sample browser) and kotlin/sample-render-host (a JVM Compose Desktop subprocess that backs the Swift demo).

Where next

  • Specification — properties and breakpoints supported across renderers.

On this page