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
childrenfor string,Node, orDocumentFragmentcontent. - 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.