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: http://localhost:7310
If busy: 7311, 7312, 7313

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 basic   # a minimal counter
npm create zoijs@latest my-app --template app     # a small task dashboard

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#

The smallest Zoijs app — a counter. One createState, one html template, one mount. Ideal when you want a blank canvas.

Both templates depend only on @zoijs/core.

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 server (npx serve .) and open the printed URL.

With a CDN (zero install)#

No package.json at all — point the import map at a CDN and pin the version:

<script type=class="tok-string">"importmap">
  { "imports": { "@zoijs/core": "https://esm.sh/@zoijs/core@1" } }
</script>

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

Next steps#