Skip to content

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: bake mutates state.cookies; the binding that reads cookies re-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.