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 dev

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

Migration 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