Component Authoring

CellUI components are plain functions that return DOM. The component runs once; signals update the exact DOM nodes they are bound to after that.

That makes reusable components different from React-style components. A CellUI component should set up the DOM once, bind signals to the smallest useful nodes, and register cleanup for external listeners or timers.

Component Shape

typescriptimport { view } from '@cmj/cellui';
import { cn } from './utils';

export interface BadgeProps {
  children?: string | Node | DocumentFragment;
  class?: string;
}

export function Badge(props: BadgeProps = {}) {
  const children = props.children ?? '';

  return view`
    <span class="${cn('cui-badge', props.class)}">
      ${children}
    </span>
  `;
}

Prefer this shape:

  • Accept a single props object.
  • Use children for string, Node, or DocumentFragment content.
  • Hoist conditional or computed template values before view.
  • Keep CSS in copy-owned token files.
  • Return a DocumentFragment.

Hoist Conditional Children

The optional Vite compiler can wrap non-signal template expressions in functions. That is useful for reactive text and attributes, but it is wrong for conditional DOM fragments.

Avoid this:

typescriptreturn view`
  <header>
    ${props.title ? view`<h3>${props.title}</h3>` : ''}
  </header>
`;

Use this:

typescriptconst title = props.title ? view`<h3>${props.title}</h3>` : '';

return view`
  <header>
    ${title}
  </header>
`;

This works with plain runtime view() and with the optional compiler transform.

Reactive Props

Use signals where the component should stay bound to caller-owned state.

typescriptimport { Signal, bind, view } from '@cmj/cellui';

export function Input(props: { value?: string | Signal<string> }) {
  return props.value instanceof Signal
    ? view`<input ${bind(props.value)} />`
    : view`<input value="${props.value ?? ''}" />`;
}

For attributes derived from signals, pass a function:

typescriptreturn view`
  <button class="${() => active.value ? 'active' : ''}">
    Save
  </button>
`;

Events And Cleanup

Template event handlers are easiest for local events:

typescriptreturn view`<button onclick="${props.onclick}">Save</button>`;

If a component attaches global listeners, timers, observers, or third-party instances, bind cleanup to a DOM node:

typescriptconst fragment = view`<div>...</div>`;
const root = fragment.firstElementChild!;

window.addEventListener('resize', onResize);
CellUI.onDispose(root, () => window.removeEventListener('resize', onResize));

return fragment;

Styling

The component registry uses cellui-ui.css and CSS custom properties. This is deliberate:

  • Components are source-owned by the app.
  • Tokens can be changed without a runtime styling system.
  • Generated components stay inspectable.

Use component classes such as .cui-button and semantic tokens such as --cui-primary, not inline style objects for reusable primitives.