Start a new app

The fastest way to a running Zoijs app is one command:

npm create zoijs@latest my-app
cd my-app
npm install
npm run dev

That scaffolds a named app, installs @zoijs/core, and serves it. npm run dev runs a tiny zero-dependency static server (no bundler, no build step):

  Zoijs dev server

  - Local:  http://localhost:7310

It falls back to 7311 / 7312 / 7313 if the port is busy.

The generated files are plain HTML, CSS, and JavaScript you can open and read.

create-zoijs is the official Zoijs starter: a tiny scaffolder, not a toolchain. It writes a few files and gets out of the way.

You don't need the CLI. Zoijs works perfectly without it — it's only a convenience. The no-CLI setup below is just as valid.

Naming your app#

The folder name you pass becomes the project name:

npm create zoijs@latest task-manager

produces a package.json with:

{ "name": "task-manager" }

and an index.html with a readable title:

<title>Task Manager</title>

If you don't pass a name, the CLI asks one simple question — Project name:.

Templates#

npm create zoijs@latest my-app                       # default: the "app" template
npm create zoijs@latest my-app --template app        # a small task dashboard
npm create zoijs@latest my-app --template basic      # a basic counter
npm create zoijs@latest my-app --template typescript # type-checked JS, no build
npm create zoijs@latest my-app --template minimal    # two files, CDN, no install
npm create zoijs@latest my-app --template library    # author a Zoijs package

app (default)#

A small task dashboard that demonstrates the whole core in one readable app: createState for the list, computed for the "tasks left" count, each for keyed rendering, and component communication — the parent passes data down to a Header, and TaskItem reports toggle/delete events back up through callbacks.

basic#

A basic counter — one createState, one html template, one mount. A small, local app with a dev server, ideal as a starting point.

typescript#

The basic counter as type-checked JavaScript: every file starts with // @ts-check, and a strict tsconfig.json plus the types shipped in @zoijs/core give you full TypeScript safety and npm run typecheck — with no build step (the browser runs the .js as-is). TypeScript-grade safety without a compiler.

minimal#

The smallest scaffold — two flat files (index.html + app.js) that load @zoijs/core from a CDN (exact version, integrity-pinned). No install — npm run dev runs the same hardened dev server as the other templates. A true blank canvas.

library#

A starter for authoring a Zoijs-based package — src/index.js plus a hand-written index.d.ts, an exports/types package.json with a peer dependency on @zoijs/core, and a node:test suite. The same shape the official optional packages use.

The app, basic, and typescript templates depend only on @zoijs/core; minimal uses no install at all; library builds on the core as a peer dependency.

Project structure#

The generated app is a handful of plain files — nothing hidden:

my-app/
  index.html            import map + the #app mount point
  package.json          app name + the dev script
  dev-server.mjs        tiny zero-dependency static server (npm run dev)
  src/
    app.js              your app: state, markup, mount
    style.css           plain CSS (light/dark, responsive)
    components/          (app template only) small components
      Header.js          parent to child: hero + summary
      StatCard.js        parent to child: a reusable stat card
      TaskItem.js        child to parent: reports events via callbacks

The basic template is the same without the components/ folder. Open any file and read it — that's the whole project.

What the CLI does#

  • Copies a template and fills in your app's name (in package.json, the <title>, and the README).
  • Validates the name (npm-safe) and refuses to overwrite a non-empty folder.
  • Prints the three commands to get going.

What the CLI does not do#

By design, to keep Zoijs simple, there is no build system, bundler, compiler, JSX transform, code generator, plugin system, template DSL, or complex prompts. The generated app is plain files — nothing is hidden.

Set up by hand (no CLI)#

The CLI just saves typing. Here's the same thing by hand.

With npm#

mkdir my-app && cd my-app
npm init -y
npm install @zoijs/core

Create index.html with an import map pointing at the installed package:

<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
    <script type=class="tok-string">"importmap">
      { "imports": { "@zoijs/core": "./node_modules/@zoijs/core/src/index.js" } }
    </script>
  </head>
  <body>
    <div id=class="tok-string">"app"></div>
    <script type=class="tok-string">"module" src=class="tok-string">"./app.js"></script>
  </body>
</html>
// app.js
import { html, mount, createState } from "@zoijs/core";

function App() {
  const count = createState(0);
  return html`<button onclick=${() => count.set(count.get() + 1)}>Clicked ${() => count.get()} times</button>`;
}

mount(App, "#app");

Serve it with any static file server (for example python3 -m http.server 7310 --bind 127.0.0.1) and open it.

With a CDN (zero install)#

No package.json at all — point the import map at exact CDN file URLs, with an integrity hash for every module (regenerate the whole map to change versions; see Installation):

<script type=class="tok-string">"importmap">
  {
    "imports": {
      "@zoijs/core": "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/index.js"
    },
    "integrity": {
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/boundary.js": "sha384-sgP+7Uk+gh1Gk39OqMxZEo9Mox6uM6upgCbk9n4HKa8L3LywliSiXjQPQrjj/OG/",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/brand.js": "sha384-SQKAavcGif93XT58P5unWneM/VMR1rPsGM/wF63kNN4YaR2RdK2mu2IwULo9VYix",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/each.js": "sha384-ple5ogdJi5exkzQbgaQ8jnLLOymkmnG8dG/B8L36QkDbmIOiuRL+IdNVNNLYh+3N",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/html.js": "sha384-5sS5ABTdRpfXmhwZuIyhqcSO7fCY1VFVlYcolWRfLsRVzytc9JnUkjfX2Adfy8WI",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/mount.js": "sha384-eAm8c9K1SY4j66NnZ4pc4exzlek7iG67di9hU5U9S3PnaZnElkqRGpzOy2z4Uz2h",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/renderer.js": "sha384-H2K6FI3+kv2iPxyLb8V9e9Ux4X95PcPQGYu/h7Gq9rk4brE1nLHKFvI4iG5vt7Uz",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/index.js": "sha384-dDKK+RVA9Pj7X7aJvv0zgepWNZQi9W20zZ1JgACZ4EbIQ9LjvLsss8gsQGiyvcRb",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/prod.js": "sha384-2Wg5h5XW9gULwwZPet1azoni63tKm9iTs9FrAoLV0vGUa77h5WPgqsG/F3TFPuBy",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/computed.js": "sha384-nlAK86Jy1IR3yFSePE38l6cjZqcu5SdnYRVQxJh/+2YQL3WhTHdZ18yCKeKL4ffm",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/core.js": "sha384-dBKT0ohkuylmZHrv0HAjRtTxrMNyo6FZTk8Fqf6LH+aAi3moXikSLGdlPaeZi5IV",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/devtools.js": "sha384-KyEdZp4AcE9RdgocIKg0Q3NKTIyCUPnMi8LLPGrDDhRCJ55DA/JcqSZRFjoHHR5X",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/effect.js": "sha384-EMCI2Kpw6k0EUn5QPC6tJLoON+nUgQ7L25AXgIrWLmhMCLKJq/jP+C6Bk/P6gGDY",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/env.js": "sha384-41791FgWw6cq/pCRB6up+hZExngmUr1tAVxQG58SdvonImEcU6jK7g14jzZAPBFL",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/owner.js": "sha384-r8/NbofI3PXPfKvjxBuhWcRZGMJQn+Ldbne0ghFkjELu6SjvpaWIHnxF8FuScUAp",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/runtime.js": "sha384-5CQnBBrMFUfcq+ahlgE4DX/nG0ua+Bo0l70VtRkXVPWQyvGtXoSScup7FHsQqbA2",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/state.js": "sha384-w2HVkiWmSdb8h7qqr4OViqHJrDqlD/oInr+5Lal+lSvXWu3SPuZv35WWDPsEOgUq",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/utils/dom.js": "sha384-XsdCmmxzHfpBmOdz1648tCRrAMMHbWKIzvm4Aj2VIEpWJDIIaKmihqEARvxN6d+2",
      "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/utils/security.js": "sha384-7UafFG7FtgE/ZSVl+gEOKHc7qewaF0V6hKLEqgI9yj9mGTxz62GTzkrUPfbOXOtT"
    }
  }
</script>

Everything else is the same. This is the most minimal possible setup.

Next steps#