Overview
Render Joy DOM documents as native SwiftUI views via a Yoga-based flex engine.
JoyDOM (Swift package) decodes a Joy DOM JSON document into a Spec model and renders it as a live SwiftUI view tree. Layout is delegated to the Yoga-based FlexLayout engine, so flex behavior matches the web spec. UIKit and WebKit are available per custom component for cases SwiftUI can't cover.
Status: pre-release
The package builds and ships a snapshot-test suite, but it isn't published to a registry yet. Its
flex engine comes from the j0yhq/flexbox-swift repo,
pinned by revision in Package.swift; folding FlexLayout into this monorepo is a planned
cleanup. Consume it today by adding the local package at swift/.
Render a document
JoyDOMView is the entry point. Build a ComponentRegistry and pass it alongside the Spec:
import SwiftUI
import JoyDOM
// ComponentRegistry.shared is empty by design. Seed a registry with the
// built-in factories (div / p / h1–h6 / span / img) via withDefaultPrimitives(),
// or every built-in node renders as a placeholder.
let registry = ComponentRegistry().withDefaultPrimitives()
struct InvoiceScreen: View {
let spec: Spec
var body: some View {
JoyDOMView(spec: spec, registry: registry)
.viewport(Viewport(width: 520)) // drives breakpoint resolution
.padding()
}
}JoyDOMView walks the tree, resolves the style cascade, and renders each node: flex containers through a FlexLayout container, text nodes through SwiftUI Text, and img nodes through AsyncImage. Without a Viewport, only document-level styles apply — breakpoints need the width, orientation, and print flag it carries.
Custom components
Register a handler keyed by the kebab-case node type. The factory receives the resolved props and an events sink, and returns a ComponentBody — .custom { … } for SwiftUI:
registry.register("contact-button") { props, events in
.custom {
Button(props.string("label") ?? "") {
events.emit("tap")
}
}
}register is last-wins and chainable, so a custom "img" overrides the built-in one. props.string("key") reads a scalar; props.value("key") returns the raw JSON value. For UIKit or web content, return .uiKit(make:update:) or .webView(html:baseURL:onMessage:) instead of .custom.
Events
Built-in nodes carry action bindings; custom components call events.emit(_:). Both bubble to the handlers you attach with .onEvent:
JoyDOMView(spec: spec, registry: registry)
.onEvent("checkout") { event in
print("checkout", event.payload) // [String: JSONValue]
}Loading documents
Spec is Codable, so decode the JSON directly:
let url = Bundle.main.url(forResource: "card", withExtension: "json")!
let data = try Data(contentsOf: url)
let spec = try JSONDecoder().decode(Spec.self, from: data)The model matches the format @joy-dom/spec defines, so a document validated on the web decodes unchanged here.
Source layout
The Swift sources live under swift/Sources/:
Sources/JoyDOM— the renderer:Specmodel, style cascade,ComponentRegistry, and the SwiftUI views.Sources/JoyDOMSampleSpecs— per-property JSON sample specs pluswithTestAssetImageFactory(), which resolvestest-assets://<name>to PNGs underassets/images/. This is a dev/test target, not a shipped product.Tests/JoyDOMTests— unit and snapshot tests with baselines underPropertyCoverage/**/__Snapshots__.
Local development
From swift/:
swift build
swift testSnapshot baselines for the per-property coverage walk are excluded from SwiftPM's resource handling via Package.swift.
Where next
- Specification — properties and breakpoints supported across renderers.