Appearance
Simple clicker
The smallest complete game: a counter, a click, and reactive DOM. Everything else in IdleKit builds on this skeleton.
HTML
html
<main>
<h1>Cookies: <span id="count">0</span></h1>
<button id="clicker">Bake a cookie</button>
</main>
<script type="module" src="/src/main.ts"></script>main.ts
ts
import { createEngine, formatInteger } from "@idlekitjs/core";
import { Renderer, bindText } from "@idlekitjs/dom";
import { createRafScheduler } from "@idlekitjs/browser/raf-scheduler";
interface State {
cookies: number;
perClick: number;
}
const renderer = new Renderer();
const engine = createEngine<State>({
initialState: { cookies: 0, perClick: 1 },
renderer,
scheduler: createRafScheduler(),
});
// --- actions -------------------------------------------------------------
function bake(state: State): void {
state.cookies += state.perClick;
}
// --- UI ------------------------------------------------------------------
const countEl = document.querySelector<HTMLElement>("#count")!;
const clickerEl = document.querySelector<HTMLButtonElement>("#clicker")!;
renderer.add(bindText(countEl, () => formatInteger(engine.state.cookies)));
clickerEl.addEventListener("click", () => bake(engine.state));
engine.start();What's happening
- The state is reactive:
bakemutatesstate.cookies; the binding that readscookiesre-runs on the next frame. Nothing else re-renders — with a static screen the game does zero DOM work. formatInteger(from core) gives locale-ready number display for free.- No system yet — a pure clicker has no passive simulation. The loop still runs (and will matter the moment you add one).
Add passive income
One system turns the clicker idle:
ts
engine.addSystem((state, dt) => {
state.cookies += state.ovens * dt; // add `ovens: number` to State
});From here, continue with idle production (cost curves and bulk buying) or save & load.