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 DOM

How 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:

  1. Joins the strings with placeholder markers (__nex_0__, __nex_1__, ...)
  2. Sets the result as innerHTML of a <template> element
  3. Clones the template content as a DocumentFragment
  4. Walks the fragment with querySelectorAll('*') for attribute bindings
  5. Walks with TreeWalker for text node bindings
  6. 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 updates

Signals

typescriptconst count = signal(0);
view`<p>Count: ${count}</p>`;
// Renders: <p>Count: 0</p>
// Reactive — updates when count.value changes

When 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 change

Functions 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 fragment

You 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>&lt;script&gt;alert("xss")&lt;/script&gt;</div>
// The script tag is NOT executed

This 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.