Skip to content

Custom components

When the starter set does not cover what you need, register a custom component. It works without forking the framework and without a Node build, because the shell ships pre-built: a custom type ships a declarative render spec the shell's generic renderer interprets at runtime, not new Svelte code.

A worked example: a colour picker

import indah

indah.register_component(
    "colorpicker",
    render={
        "tag": "input",  # one allowlisted HTML element
        "attrs": {"type": "color"},  # static attributes
        "bind": {"value": "value"},  # element attribute <- node prop
        "on": {"input": {"event": "input", "prop": "value"}},  # UI change -> signal
    },
)

colour = indah.Signal("#ff8800")
picker = indah.custom("colorpicker", value=colour)  # two-way bound, like a built-in

register_component() validates the spec once; custom(type, **props) builds value-bearing instances that round-trip exactly like a built-in component. Read the current value with colour.value - it follows what the user picked.

The render spec

Field Meaning
tag The HTML element to create, from a safe allowlist (no script / iframe / style)
attrs Static attributes {name: value}
class A static class string
text A node prop whose value becomes the element's text content
bind {attribute: prop} - the element attribute follows the node prop, patched reactively
on {domEvent: {event, prop}} - a DOM event posts the indah event; prop names the signal the value is written into
children Nested render specs, for a small composite

What the spec can and cannot do

The spec is data, not code, and the tag set is inert, so a custom component cannot inject a script or fetch remote code. That keeps indah's dev-tool threat model intact - registering a component never widens the attack surface.

No loops or conditionals, by design

The spec deliberately cannot express loops or conditionals. A component that needs those is the signal to reach for a hand-written frontend against the protocol, not a larger spec language.

Validation is strict: a disallowed tag, a type that collides with a built-in, an empty type, or a malformed spec is rejected with a ValueError at registration time, not at render time.

How it travels

The spec rides on the wire as a reserved _spec prop, so it is part of the public protocol. The shell's generic renderer reads _spec, builds the element, binds its attributes to node props, and wires its DOM events back to your signals - all at runtime, with no rebuild.