Adoption
How to Start
CellUI doesn't require you to rewrite your app. You can adopt it one widget at a time.
Greenfield: new project
bashnpm create cellui@latest my-app
cd my-app && npm install && npm run devThis scaffolds a Vite + TypeScript project with CellUI configured.
Existing HTML page: script tag
No build step required:
html<div id="widget"></div>
<script type="module">
import { signal, view, mount } from 'https://esm.sh/@cmj/cellui';
const count = signal(0);
mount('#widget', () => view`
<button onclick="${() => count.value++}">Clicks: ${count}</button>
`);
</script>This works in any HTML page — WordPress, Rails, Django, static sites. No bundler, no build, no framework migration.
Inside a React app
Mount CellUI into a React component using useEffect and useRef:
tsx// React component that embeds a CellUI widget
import { useEffect, useRef } from 'react';
function CellUIWidget() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
// Dynamic import to avoid bundling CellUI with React
import('@cmj/cellui').then(({ signal, view, mount }) => {
const count = signal(0);
mount(ref.current!, () => view`
<button onclick="${() => count.value++}">
CellUI counter: ${count}
</button>
`).then(killSwitch => {
// Clean up when React unmounts this component
return () => killSwitch();
});
});
}, []);
return <div ref={ref} />;
}CellUI manages its own DOM inside the ref container. React manages everything outside. They don't conflict because CellUI doesn't touch the virtual DOM.
Inside a Vue app
Same pattern — mount into a ref:
vue<template>
<div ref="cellui"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const cellui = ref(null);
let killSwitch;
onMounted(async () => {
const { signal, view, mount } = await import('@cmj/cellui');
const count = signal(0);
killSwitch = await mount(cellui.value, () => view`
<button onclick="${() => count.value++}">Count: ${count}</button>
`);
});
onUnmounted(() => killSwitch?.());
</script>WordPress / PHP
Add to your theme's footer.php or via a shortcode:
html<div id="cellui-calculator"></div>
<script type="module">
import { signal, computed, view, mount } from 'https://esm.sh/@cmj/cellui';
const price = signal(0);
const quantity = signal(1);
const total = computed(() => price.value * quantity.value);
mount('#cellui-calculator', () => view`
<div>
<input type="number" value="0" oninput="${(e) => price.value = +e.target.value}" placeholder="Price" />
<input type="number" value="1" oninput="${(e) => quantity.value = +e.target.value}" placeholder="Qty" />
<p>Total: ${total}</p>
</div>
`);
</script>No npm, no bundler, no node_modules. Works in any PHP template.
Multiple Widgets on One Page
Each mount() call is independent. You can have multiple CellUI widgets on the same page, each with their own state:
html<div id="counter"></div>
<div id="search"></div>
<script type="module">
import { signal, view, mount } from '@cmj/cellui';
// Widget 1: Counter
mount('#counter', () => {
const count = signal(0);
return view`<button onclick="${() => count.value++}">Count: ${count}</button>`;
});
// Widget 2: Search
mount('#search', () => {
const query = signal('');
return view`<input oninput="${(e) => query.value = e.target.value}" placeholder="Search..." />`;
});
</script>To share state between widgets, use ambient():
typescriptimport { ambient } from '@cmj/cellui';
const user = ambient('user', null);
// Both widgets can read and write user.valueMigration Path
From React
| React | CellUI |
|---|---|
useState(0) |
signal(0) |
useMemo(() => x, [dep]) |
computed(() => x) |
useEffect(() => {}, [dep]) |
effect(() => {}) |
useContext |
ambient() |
useRef |
document.createElement or signal |
<Component prop={val}> |
Component({ prop: val }) |
{condition && <X/>} |
when(condition, () => X()) |
{items.map(i => <I/>)} |
each(items, 'id', i => I(i)) |
createPortal |
portal() |
<Suspense> |
when(loading, ...) |
The key mental shift: React components re-execute. CellUI components execute once. Don't put signal creation inside conditions or loops — put it at the top of the function and let reactivity handle updates.
From Vue
| Vue | CellUI |
|---|---|
ref(0) |
signal(0) |
computed(() => x) |
computed(() => x) |
watch(() => {}) |
effect(() => {}) |
provide/inject |
ambient() |
v-model |
bind() |
v-if |
when() |
v-for |
each() |
| Template syntax | Tagged template literals |
Vue and CellUI share similar reactivity concepts. The main difference: Vue components are objects with lifecycle hooks. CellUI components are functions that return DOM.
When NOT to Adopt CellUI
You need a large component library today — CellUI doesn't have one. If your project starts tomorrow and needs 50 ready-made components, use React + shadcn or Vue + Vuetify.
Your team is 20+ React developers — the switching cost outweighs the performance benefit. Use CellUI for new isolated features, not a full rewrite.
You need SSR with streaming — CellUI's
serverView()is synchronous. For streaming SSR or React Server Components, stay with Next.js or Remix.You're building a content site — use Astro, Hugo, or Eleventy. CellUI is for interactive UIs, not static content rendering.
When CellUI Wins
- Embedded widgets in existing sites (calculators, configurators, dashboards)
- Performance-critical UIs where React's reconciliation overhead shows (real-time data, large tables)
- Small-to-medium SPAs where you want to own every line of code
- Teams that value simplicity — 22 exports, no build step required, no framework magic