Kotlin DSL
Build Spec documents in type-safe Kotlin instead of decoding JSON, with the optional dom-dsl module.
The optional dom-dsl module builds Spec objects programmatically with a type-safe Kotlin builder DSL — handy for tests, inline content, or anywhere a JSON string is awkward. It produces the same Spec the JSON decoder does, so the output renders, resolves, and serializes identically.
dependencies {
implementation("com.j0y.joy:dom-dsl:1.0.0")
}The spec { } entry point
Every document starts with the top-level spec { } builder. Inside it you declare style entries, breakpoints, and exactly one root layout:
import com.j0y.joy.dom.dsl.*
val document = spec {
style("div") {
color = "#111111".color
padding(4.px)
}
style(".card") {
color = "#222222".color
margin(8.px)
}
layout {
div {
props {
id = "hero"
className("card")
}
+"Hello, Joy-DOM!"
}
}
}version defaults to 1 (the only valid value) and rarely needs setting. build() throws if no layout was declared — the spec requires a root node.
Declaring nodes
Inside layout { } (and inside any node body), use the built-in HTML tag helpers — div, span, p, img, h1–h6. Each opens a nested NodeBuilder:
layout {
div {
h1 { +"Title" }
p {
+"A paragraph with "
span { +"an inline run" }
}
img { props { id = "photo" } }
}
}The root layout { } must contain exactly one top-level node — a second one throws.
For custom (kebab-case) component types, use the generic node(type) { }:
layout {
node("rating-stars") {
props { /* ... */ }
}
}node(type) also exists as a free function that returns a standalone Node (val n = node("div") { ... }), and layout(node) / layout(type) { } overloads let you set the root from a prebuilt node or by raw type string.
Props: id, classes, style, events
A node's props { } block configures its id, class list, inline style, and event bindings:
div {
props {
id = "hero"
className("card", "primary") // accumulates across calls
style {
display = Display.Flex
alignItems = AlignItems.Center
}
onClick("charge") {
param("source", "stripe")
param("amount", 1000)
}
}
}className(vararg)appends classes; calling it again adds more.style { }sets the node's inline style (same builder as the top-levelstyle(selector) { }).onClick/onFocus/onBlur/onChangebind a named action, with an optionalparams { }block.param(key, value)acceptsString,Number,Boolean, or a rawJsonElement. You can also assign anEventdirectly:onBlur = Event(name = "blur").
Unknown sibling keys are preserved verbatim via extras(key, value) on both props { } and the node body.
Children: elements, text, numbers
Add element children with the tag helpers or node(type) { }. Add text and other leaf children with the unary + operator and explicit helpers:
p {
+"Plain text" // String → text child
text("also text") // explicit text
number(42) // numeric child (stored as Double)
number(3.14)
nullChild() // JSON null child
}A node with no child added serializes children as absent (null), distinct from an empty list — preserving the spec's absent-vs-empty distinction.
Styles, lengths, and colors
The style { } builder exposes every supported CSS property as a typed var (display, flexDirection, justifyContent, width, backgroundColor, fontSize, color, objectFit, …). Enums use their Kotlin types: Display.Flex, AlignItems.Center, JustifyContent.SpaceBetween.
Lengths come from extension properties on Int/Double:
style {
width = 100.percent
height = 50.5.percent
fontSize = 24.px
gap = 8.px
}Spacing and radius have shorthand helpers (1-, 2-, and 4-value forms):
style {
padding(8.px) // all sides
padding(vertical = 10.px, horizontal = 20.px)
margin(top = 0.px, right = 4.px, bottom = 0.px, left = 4.px)
borderRadius(topLeft = 1.px, bottomRight = 3.px)
}Colors parse from CSS strings via the .color extension, or build directly with constructors:
color = "#111111".color // hex / rgb / hsl / named string
backgroundColor = rgb(255, 0, 0)
borderColor = rgba(0, 0, 0, 0.5)
// also: hex(...), hsl(h, s%, l%), lab(...), oklch(...)Breakpoints and media queries
breakpoint { } adds a responsive override. Declare match conditions { } with the media-query builder, then per-id node prop overrides and per-selector style overrides:
spec {
style(".card") { padding(8.px) }
breakpoint {
conditions {
width(min = 768.px) // min-width: 768px
}
nodes("hero") {
style { color = "#ffffff".color }
}
style(".card") {
padding(16.px)
}
}
layout {
div {
props { id = "hero"; className("card") }
+"Responsive"
}
}
}The media-query builder covers width(min =, max =) (and an explicit-operator overload), landscape() / portrait(), print(), plus and { }, or { }, and not { } to combine and negate conditions.
The DSL mirrors the spec, not a superset
Every DSL construct maps 1:1 to a Spec field. There are no DSL-only features — anything you can
build here you can also write as JSON, and round-trips through JoyDomJson are lossless.
What's next
- HTML & CSS converter — import existing markup into a
Spec, or export one back to HTML. - Specification — the properties and breakpoints the DSL builds.
- API reference (Dokka) ↗ — the full
dom-dslbuilder surface.