Node builders
Build UI as a tree of nodes, from simple elements to lists, navigation, and components.
Start with nodes
Thorm UIs are composed from nodes. A node is a structured description of a DOM element, a text node, or a higher-level construct like a fragment, list, or component. When you call a node builder you are not generating HTML strings; you are building an IR tree that Thorm can render and update.
Thorm supports CSR (Client Side Rendering) and SSR (Server Side Rendering). It started as CSR, and you can render a complete client-side page today without being limited by SSR. SSR is an additional mode, not a constraint on how you build nodes.
The core trio: el, text, fragment
These three functions describe the structure of most UIs. You will use them constantly and they map directly to the node types in the IR tree.
- el(tag, props, children) creates an element node with attributes, classes, events, and child nodes.
- text(value) creates a text node. Use it anywhere you need plain text in the tree.
- fragment(children) groups nodes without adding an extra wrapper element to the DOM.
Writing el('tagname', [...], [...]) all the time gets tedious. For convenience, Thorm maps common HTML tags to helpers like div([props], [children]). In these tutorials we stick to el('tagname', [], []) because it is the base form and it works for every tag, including the ones that are not mapped.
A good mental model is that each call to a node builder returns one node. The IR is just the tree of those nodes, and Thorm renders that tree in CSR, SSR, or both.
Conditions and lists
Once you can build static structure, you will want to show or hide nodes and repeat patterns over data. Thorm has node builders for both.
- show(cond, node) renders a node only when the condition is true. The condition is an expression, which keeps the IR explicit and reactive.
- repeat(items, key, tpl) renders a list by evaluating a template node for each item. Use stable keys (like ids) so the runtime can reconcile efficiently.
The list template is still just a node. You do not pass callbacks; you pass a node that can read item fields with item().
Composition with components and slots
Components in Thorm are still node builders. A component is a template node that reads props and renders slots. This keeps composition explicit and makes it easy to reuse layouts.
- component(tpl, props, slots) creates a component instance.
- prop(name) reads a prop inside the component template.
- slot(name) is a placeholder for children passed from the parent.
Slots are nodes just like everything else. This means a slot can contain complex trees, conditionals, lists, and even other components.
Example: repeat() a list
This example renders a list using repeat(). The items are stored in an atom and read as an expression, which keeps the IR reactive and update-friendly.
Notice that the list template is a node (the li element). Inside that template, item('name') reads the current item. The key is item('id'), which gives Thorm a stable identity for each row so updates and reorderings are precise.
Example: component props and slots
This example builds a reusable card template. The template reads a prop called title and exposes a default slot where the parent can inject content.
The body slot is built as a node tree. It contains a button wired to inc() and a paragraph with an anchor tag. Because the prop value is concat('Hello, Props! ', read($i)), the title updates whenever the button increments the state.
Advanced notes and practical guidance
- Keys drive diffing. Stable keys let Thorm move or update nodes instead of recreating them.
- Use fragment() when you want multiple siblings without an extra wrapper element.
- html() is an escape hatch for raw HTML. Use it only with trusted content.
- A component template is just a node tree. This makes components easy to test and reason about because there is no hidden magic outside the tree.
If you are coming from template-driven systems, the key shift is that everything is a node, including components, conditions, and lists. Once you internalize that, the rest of the API feels consistent and predictable.