Templating
What You're Used To
React uses JSX — an XML-like syntax that compiles to createElement calls. Vue uses single-file components with <template> blocks. Svelte uses its own .svelte format. All require a build step to transform the template syntax into JavaScript.
CellUI uses tagged template literals — a native JavaScript feature that works without any compiler or build step:
typescriptimport { view } from '@cmj/cellui';
const greeting = view`<h1>Hello, world</h1>`;
// greeting is a real DocumentFragment, ready to insert into the DOMHow view Works
view is a tagged template function. When you write view`<h1>${name}</h1>` , JavaScript calls view(strings, ...values) with:
strings:['<h1>', '</h1>']values:[name]
CellUI then:
- Joins the strings with placeholder markers (
__nex_0__,__nex_1__, ...) - Sets the result as
innerHTMLof a<template>element - Clones the template content as a
DocumentFragment - Walks the fragment with
querySelectorAll('*')for attribute bindings - Walks with
TreeWalkerfor text node bindings - Replaces each placeholder with the actual value — binding signals, attaching event handlers, or inserting static text
The result is a real DOM tree. Not a virtual representation. Not a description that needs to be reconciled. A real DocumentFragment that you can appendChild directly.
Interpolation Types
CellUI handles five types of interpolated values:
Static values
typescriptconst name = 'Alice';
view`<p>Hello, ${name}</p>`;
// Renders: <p>Hello, Alice</p>
// Static — never updatesSignals
typescriptconst count = signal(0);
view`<p>Count: ${count}</p>`;
// Renders: <p>Count: 0</p>
// Reactive — updates when count.value changesWhen CellUI sees a Signal in a text position, it calls signal.bindNode(textNode, updater). The text node is held via WeakRef. When the signal changes, only that text node updates.
Computed functions
typescriptconst count = signal(0);
view`<p>Doubled: ${() => count.value * 2}</p>`;
// Renders: <p>Doubled: 0</p>
// Reactive — re-evaluated via effect() when dependencies changeFunctions are wrapped in effect() automatically. The function re-runs whenever any signal it reads changes, and the text node updates with the new return value.
Event handlers
typescriptview`<button onclick="${() => count.value++}">+</button>`;
// The function is attached via addEventListener('click', fn)
// The onclick attribute is removed from the DOM (clean HTML)CellUI detects on* attributes, extracts the function, calls addEventListener, and removes the attribute. This works for any DOM event: onclick, oninput, onsubmit, onkeydown, etc.
DOM nodes
typescriptconst child = document.createElement('strong');
child.textContent = 'bold';
view`<p>This is ${child}</p>`;
// The <strong> element is inserted directly into the fragmentYou can nest DocumentFragment results from other view calls, enabling component composition.
Attribute Binding
Signals in attribute positions bind reactively:
typescriptconst cls = signal('active');
view`<div class="${cls}">content</div>`;
// class="active" initially
// When cls.value = 'hidden', the attribute updates to class="hidden"CellUI calls signal.bindNode(element, updater) where the updater calls setAttribute. It skips the DOM write if the new value equals the current attribute value (optimization).
Components
A CellUI component is a function that returns a DocumentFragment:
typescriptfunction Button(label: string, onClick: () => void) {
return view`<button onclick="${onClick}">${label}</button>`;
}
function App() {
const count = signal(0);
return view`
<div>
<h1>Count: ${count}</h1>
${Button('Increment', () => count.value++)}
${Button('Reset', () => count.value = 0)}
</div>
`;
}There's no component registration, no props validation, no lifecycle. A component is a function. Composition is function calls. This is just JavaScript.
Control Flow
CellUI provides three control flow primitives. They return DocumentFragment nodes that you interpolate into templates:
when(condition, views)
Conditional rendering:
typescriptimport { when } from '@cmj/cellui';
view`
${when(isLoading, {
true: () => view`<div class="spinner">Loading...</div>`,
false: () => view`<div>Content loaded</div>`,
})}
`;when accepts a signal or a function. If you pass a function, CellUI wraps it in effect() so it re-evaluates when dependencies change:
typescriptwhen(() => items.value.length > 0, {
true: () => view`<p>${() => items.value.length} items</p>`,
false: () => view`<p>No items</p>`,
});When the condition changes, when removes the old nodes and inserts the new ones. CSS animations on the outgoing nodes are respected — removal is deferred until getAnimations() completes.
each(list, key, render)
Keyed list rendering:
typescriptimport { each } from '@cmj/cellui';
view`
<ul>
${each(todos, 'id', (todo) => view`
<li>${todo.text}</li>
`)}
</ul>
`;each maintains a cache of DOM nodes keyed by the key property (or a key function). When the list signal changes:
- New items are created
- Removed items are removed (with animation deferral)
- Reordered items are moved in the DOM (not recreated)
This is O(n) in the list size but O(1) per item — each item's DOM nodes are reused, not rebuilt.
match(signal, cases, default)
Pattern matching / routing:
typescriptimport { match } from '@cmj/cellui';
const page = signal<'home' | 'about' | 'contact'>('home');
view`
${match(page, {
home: () => view`<h1>Home</h1>`,
about: () => view`<h1>About</h1>`,
contact: () => view`<h1>Contact</h1>`,
}, () => view`<h1>404</h1>`)}
`;match is a switch statement for your UI. When the signal value changes, it removes the current nodes and renders the matching case. The optional last argument is the default case.
XSS Safety
Signal values interpolated in text positions use textContent, not innerHTML. HTML in signal values is rendered as escaped text:
typescriptconst userInput = signal('<script>alert("xss")</script>');
view`<div>${userInput}</div>`;
// Renders: <div><script>alert("xss")</script></div>
// The script tag is NOT executedThis is safe by default. If you need to render raw HTML, use innerHTML on a container element directly — but understand the security implications.
No Build Step Required
The view function works in any JavaScript environment. No JSX transform, no compiler plugin, no .svelte file format. You can load CellUI from a CDN and use it in a plain <script> tag:
html<script type="module">
import { signal, view } from 'https://esm.sh/@cmj/cellui';
const count = signal(0);
document.body.appendChild(
view`<button onclick="${() => count.value++}">
Clicked ${count} times
</button>`
);
</script>The Vite plugin (vite-plugin-cellui) adds optional build-time template optimization via a Rust compiler, but it's not required. CellUI works without it.