Getting started

Four steps from nothing to a working reactive component. The first three need no tooling at all; step four is the npm path for bundled projects.

1. Install

Pick one. The script tag is not a demo mode: it is a production path.

Option A, a script tag. No package manager, no build:

<script src="https://cdn.jsdelivr.net/npm/lemonadejs@6/dist/lemonade.min.js"></script>

Option B, npm. For bundlers and TypeScript projects:

npm install lemonadejs

2. Create one file

Save this as index.html. It is the entire application: no compiler, no CLI, no scaffold. The result panel above the code is this exact file running on this page.

live
<!doctype html>
<html>
<body>
    <div id="app"></div>
    <script src="https://cdn.jsdelivr.net/npm/lemonadejs@6/dist/lemonade.min.js"></script>
    <script>
    const { html, mount } = lemonade;

    const Counter = (props, { state }) => {
        const count = state(0);
        return html`<div>
            <h1>${count}</h1>
            <button onclick="${() => count.value++}">+1</button>
        </div>`;
    };

    mount(Counter, document.getElementById('app'));
    </script>
</body>
</html>

3. Open it in a browser

Double-click the file, or serve the folder:

npx serve .

You should see a number and a button, and the number goes up when you click. That is a reactive state, an event, and a mounted component: the three primitives you will use every day.

  • state(0) creates a reactive box; ${count} in the template is live (State).
  • onclick="${() => ...}": events are always functions, never strings (Events).
  • mount() returns { el, unmount }; unmounting disposes everything (Lifecycle).

4. Or use npm

The same component as a module. TypeScript declarations ship in the package:

import { html, mount, type Component } from 'lemonadejs';

const Counter: Component = (props, { state }) => {
    const count = state(0);
    return html`<div>
        <p>${count}</p>
        <button onclick="${() => count.value++}">+1</button>
    </div>`;
};

mount(Counter, document.getElementById('app'));

The package’s exports map serves the dev build (warnings on) under the development condition and the stripped production build otherwise. webpack and Vite set that condition from their mode automatically; with raw esbuild, pass it yourself in development:

npx esbuild src/app.ts --bundle --outfile=dist/app.js                 # production
npx esbuild src/app.ts --bundle --conditions=development --watch ...  # development

Dev build vs production

One file, two builds. dist/lemonade.min.js is the production IIFE (global lemonade, all dev-only code eliminated, so production pays zero). During development load dist/lemonade.dev.js instead: the same API plus every warning and check (LJS-* codes, explain()).

<!-- development: warnings and checks on -->
<script src="https://cdn.jsdelivr.net/npm/lemonadejs@6/dist/lemonade.dev.js"></script>

With the dev build loaded, mistakes answer back: mutate without touch() and LJS-201 tells you; misspell a registered component and LJS-104 names it; write onClick and LJS-305 corrects the casing. Ship the silent, smaller production build.

The complete API fits in one machine-readable file, which states “if something is not here, it does not exist.” Paste it into any agent:

llms.txt: the API in ~2k tokens

Where to go next

Read Templates first: four rules govern every $, and everything else follows from them. State, Events, Two-way binding and Components are the daily working set; Lifecycle and Destroy cover how things mount and, provably, die. When a component becomes a product, Contracts is the publishing layer, Tests is the proof, and React and Web components are the doors it ships through.