Components

Everything in Kraft is a component. Here's how they work.

The component model

Every Kraft component is a Kotlin class that extends Component<PROPS> and implements one method: render().

class Greeting(ctx: Ctx<Props>) : Component<Greeting.Props>(ctx) {
    data class Props(val name: String)

    override fun VDom.render() {
        h2 { +"Hello, ${props.name}!" }
    }
}

Alongside the class, you define a factory function so other components can use it in the HTML DSL:

@Suppress("FunctionName")
fun Tag.Greeting(name: String) = comp(
    Greeting.Props(name = name)
) { Greeting(it) }

Now any parent component can write:

override fun VDom.render() {
    Greeting(name = "World")
}

Component types

Component with props

The most common type. Props are a data class — immutable, type-safe, and IDE-friendly.

class UserCard(ctx: Ctx<Props>) : Component<UserCard.Props>(ctx) {
    data class Props(
        val name: String,
        val email: String,
        val role: String = "member",  // Default values work
    )

    override fun VDom.render() {
        ui.card {
            noui.content {
                ui.header { +props.name }
                noui.description { +props.email }
            }
            noui.extra.content {
                ui.label { +props.role }
            }
        }
    }
}

PureComponent (no props)

For components that don't need external data — pages, layouts, root components:

class DashboardPage(ctx: NoProps) : PureComponent(ctx) {
    override fun VDom.render() {
        ui.container {
            ui.header H1 { +"Dashboard" }
            // ...
        }
    }
}

PureComponent is a convenience alias for Component<Any?> with NoProps context.

Functional components

For simple, stateless rendering — no class needed:

val Badge = component { name: String, color: String ->
    ui.with(color).label { +name }
}

// Usage
override fun VDom.render() {
    Badge("Admin", "red")
    Badge("Active", "green")
}

Functional components support up to 10 parameters.

Lifecycle hooks

Components register lifecycle handlers in their init block via the lifecycle DSL — onMount, onUpdate, onUnmount, onNextProps, onError (error boundaries), onResize, and browser window events.

init {
    lifecycle {
        onMount { /* component is in the DOM */ }
        onUpdate { /* DOM updated after re-render */ }
        onUnmount { /* component removed */ }
        onNextProps { new, old -> /* parent sent new props */ }
        onError { e -> /* caught from render or child */ }
    }
}

See the Lifecycle Hooks page for the full reference with examples for each hook.

Key point: Stream subscriptions (via subscribingTo()) auto-unsubscribe when the component unmounts. You don't need to clean them up manually.

Component references

Sometimes a parent needs to call methods on a child. Use ComponentRef:

class DrawingApp(ctx: NoProps) : PureComponent(ctx) {

    // Create a ref tracker for the SignaturePad component
    private val padRef = ComponentRef.Tracker<SignaturePad>()

    override fun VDom.render() {
        // The SignaturePad component, tracked by our ref
        SignaturePad {
            // configuration...
        }.track(padRef)

        // Use the ref to access the child component
        padRef { pad ->
            ui.button {
                onClick { pad.clear() }
                +"Clear signature"
            }

            if (pad.isEmpty()) {
                ui.red.label { +"No signature yet" }
            }
        }
    }
}

The padRef { ... } block only renders when the ref is attached — it's safe by design.

Composing components

Components compose naturally through the HTML DSL:

class App(ctx: NoProps) : PureComponent(ctx) {
    override fun VDom.render() {
        div(classes = "app") {
            NavBar()
            ui.container {
                Sidebar()
                MainContent()
            }
            Footer()
        }
    }
}

Passing callbacks

Components communicate upward through callback props:

class TodoItem(ctx: Ctx<Props>) : Component<TodoItem.Props>(ctx) {
    data class Props(
        val text: String,
        val done: Boolean,
        val onToggle: () -> Unit,
        val onDelete: () -> Unit,
    )

    override fun VDom.render() {
        ui.item {
            ui.checkbox {
                input(type = InputType.checkBox) {
                    checked = props.done
                    onChange { props.onToggle() }
                }
                label { +props.text }
            }
            ui.red.icon.button {
                onClick { props.onDelete() }
                icon.trash()
            }
        }
    }
}

The shouldRedraw optimization

By default, components redraw whenever props change. Override shouldRedraw to skip unnecessary renders:

override fun shouldRedraw(nextProps: Props): Boolean {
    return nextProps != props  // Only redraw if props actually changed
}

Since Props is a data class, != does a structural comparison — this is efficient and correct.

Accessing app services

Components can access app-level services through the attribute system:

override fun VDom.render() {
    // These are available in any component
    val router = router                 // Navigation
    val modals = modals                 // Show modals
    val toasts = toasts                 // Show notifications
    val popups = popups                 // Show popups and context menus
    val responsive = responsiveCtrl()   // Screen size info
}

These are injected via Kraft's attribute system — no manual wiring needed. See Overlays for modals, toasts, and popups. See Utilities for responsive design.

Keys for list rendering

When rendering a list of items, set a key on each element so Preact can efficiently track which items changed, moved, or were removed:

ui.list {
    items.forEach { item ->
        noui.item {
            key = item.id  // stable, unique identifier
            +item.name
        }
    }
}

Without keys, Preact re-creates every element on each render. With keys, it only updates what actually changed. Use a stable identifier (database ID, unique string) — not the list index.

// Also works on components
users.forEach { user ->
    UserCard(user) // implicit key from props

    // Or set explicitly on the wrapper
    div {
        key = user.id
        UserCard(user)
    }
}

Raw HTML with unsafe

The unsafe block injects raw HTML strings directly into the DOM, bypassing the type-safe DSL:

div {
    unsafe {
        +"""<em>This is raw HTML</em>"""
    }
}
The name is a hint. Raw HTML means raw risk. If the content comes from user input, an API, or a database, you are one unsanitized string away from an XSS attack. Always sanitize with a library like DOMPurify before injecting untrusted content.
// SAFE: sanitize untrusted content before injecting
import io.peekandpoke.kraft.addons.marked.markdown2html

div {
    unsafe {
        // markdown2html uses DOMPurify internally
        +markdown2html(untrustedMarkdown)
    }
}

Key the parent of unsafe

Preact cannot diff raw HTML — it treats the entire unsafe block as an opaque blob. On each re-render, the raw DOM is destroyed and recreated from scratch. To prevent this, set a key on the parent element. If the key doesn't change, Preact preserves the DOM subtree:

div {
    key = "article-${article.id}"
    unsafe {
        +article.htmlContent
    }
}

Without the key, every component re-render rebuilds the raw HTML DOM — which is wasteful for large content and causes visible flicker. With a stable key, the DOM is only rebuilt when the key changes.

See it in action