# Chapter 1: Tooling for web games in 2026 (/guides/handmade-web-games/tooling-for-web-games-in-2026)





<RuntimeProvider>
  I'd love to be able to recommend a fully dependency-free path to building web games, but right now without a bit of tooling it's significantly more difficult. So in this chapter we'll set up a simple, reusable starting point from which you can build just about any browser game. Our examples require [Node.js](https://nodejs.org/) with npm. (Alternatives like [Bun](https://bun.sh/) and [Deno](https://deno.com/) also work.)

  ## The bundler [#the-bundler]

  **[Vite](https://vite.dev/)** is a bundler. It takes the code from your many source files and assembles them into a single "bundle", hence the name. In 2026, bundlers also do a whole lot more. Vite will also...

  * Transpile TypeScript source code into JavaScript that the browser can run.
  * Run a local dev server with live reloading and hot module replacement.
  * Generate source maps for debugging.
  * Optimize assets for production builds.
  * Allow you to import assets and optionally inline them directly into your bundled code. Images, sound effects, wasm files, and fonts are much easier to work with when using Vite.

  ## Scaffold the project [#scaffold-the-project]

  Scaffold out a minimal app by running the following command:

  <RuntimeCodeTabs>
    <RuntimeCodeTab value="npm">
      ```bash
      npm create vite@latest
      ```
    </RuntimeCodeTab>

    <RuntimeCodeTab value="bun">
      ```bash
      bun create vite
      ```
    </RuntimeCodeTab>

    <RuntimeCodeTab value="deno">
      ```bash
      deno init --npm vite
      ```
    </RuntimeCodeTab>
  </RuntimeCodeTabs>

  After naming your project in Vite's CLI, pick the "vanilla" and "TypeScript" options.

  <RuntimeOnly runtime="npm">
    ```ansi
    $ npm create vite@latest
    │
    ◇  Project name:
    │  asteroids
    │
    ◇  Select a framework:
    │  Vanilla
    │
    ◇  Select a variant:
    │  TypeScript
    │
    ◇  Install with npm and start now?
    │  No
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="bun">
    ```ansi
    $ bun create vite
    │
    ◇  Project name:
    │  asteroids
    │
    ◇  Select a framework:
    │  Vanilla
    │
    ◇  Select a variant:
    │  TypeScript
    │
    ◇  Install with bun and start now?
    │  No
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="deno">
    ```ansi
    $ deno init --npm vite
    │
    ◇  Project name:
    │  asteroids
    │
    ◇  Select a framework:
    │  Vanilla
    │
    ◇  Select a variant:
    │  TypeScript
    │
    ◇  Install with npm and start now?
    │  No
    ```
  </RuntimeOnly>

  Enter the project directory and install its dependencies:

  <RuntimeOnly runtime="npm">
    ```bash
    cd asteroids
    npm install
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="bun">
    ```bash
    cd asteroids
    bun install
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="deno">
    ```bash
    cd asteroids
    deno install
    ```
  </RuntimeOnly>

  Your project should look roughly like:

  <Files>
    <File name="index.html" />

    <File name="package.json" />

    <RuntimeOnly runtime="npm">
      <File name="package-lock.json" />
    </RuntimeOnly>

    <RuntimeOnly runtime="bun">
      <File name="bun.lock" />
    </RuntimeOnly>

    <RuntimeOnly runtime="deno">
      <File name="deno.lock" />
    </RuntimeOnly>

    <Folder name="public">
      <File name="favicon.svg" />

      <File name="icons.svg" />
    </Folder>

    <Folder name="src">
      <Folder name="assets">
        <File name="hero.png" />

        <File name="typescript.svg" />

        <File name="vite.svg" />
      </Folder>

      <File name="counter.ts" />

      <File name="main.ts" />

      <File name="style.css" />
    </Folder>

    <File name="tsconfig.json" />
  </Files>

  Go ahead and delete everything in the `src/` folder, then add a new placeholder `main.ts` for us to return to later. You can put whatever you want in there for now.

  ```ts title="src/main.ts"
  console.log("it works!");
  ```

  ### Optional configuration tweaks [#optional-configuration-tweaks]

  #### Update `vite.config.ts` [#update-viteconfigts]

  Install [`vite-plugin-image-optimizer`](https://github.com/FatehAK/vite-plugin-image-optimizer) <small className="opacity-75">\[GitHub]</small> and its peer dependencies [`sharp`](https://github.com/lovell/sharp) and [`svgo`](https://github.com/svg/svgo). The plugin will automatically compress any image assets added to your project source at build-time.

  <RuntimeOnly runtime="npm">
    ```bash
    npm install vite-plugin-image-optimizer sharp svgo
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="bun">
    ```bash
    bun add vite-plugin-image-optimizer sharp svgo
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="deno">
    ```bash
    deno add vite-plugin-image-optimizer sharp svgo
    ```
  </RuntimeOnly>

  Now create or edit `vite.config.ts` in your project root:

  <Scrollycoding>
    <slot>
      <>      </>
    </slot>

    <slot path="steps">
      Start with a minimal config.
    </slot>

    <slot path="steps">
      Add the image optimizer plugin you just installed. Import `ViteImageOptimizer` and pass it into the `plugins` array.
    </slot>

    <slot path="steps">
      <>
        Enable [source maps](https://developer.mozilla.org/en-US/docs/Glossary/Source_map) <small className="opacity-75">\[MDN]</small> with `sourcemap: true`.

        <details>
          <summary>
            Why? 

            *(A philosophical detour on source maps)*
          </summary>

          One of the defining characteristics of the web is that you can read the full source code of any webpage you load by simply clicking "View Source" or "Inspect Element" in your browser.

          However, modern websites and games use source code minification to reduce bundle sizes, which makes it difficult to learn anything interesting from the source directly.

          See for yourself. The following minified code...

          <WordWrappedCode title="example.min.js" lang="js">
            {`!function(e,t,l,n){const c=n/100,o=48*c;e.fillStyle="#1e1e2e",e.fillRect(t,l-12,48,6);const f=c<.3;e.fillStyle=f?"red":"green",e.fillRect(t,l-12,o,6)}(document.createElement("canvas").getContext("2d"),0,16,50)`}
          </WordWrappedCode>

          ...was the compressed output of this source code:

          ```js title="example.js"
          // draw a health bar above the character
          function drawHealthBar(ctx, x, y, health) {
            const maxHealth = 100;
            const barWidth = 48;
            const barHeight = 6;
            const healthPercent = health / maxHealth;
            const width = barWidth * healthPercent;

            // dark background
            ctx.fillStyle = "#1e1e2e";
            ctx.fillRect(x, y - 12, barWidth, barHeight);

            // green when healthy, red when critical
            const isCritical = healthPercent < 0.3;
            ctx.fillStyle = isCritical ? "red" : "green";
            ctx.fillRect(x, y - 12, width, barHeight);
          }

          const canvas = document.createElement("canvas");
          const ctx = canvas.getContext("2d");

          let currentHealth = 50;
          let x = 0;
          let y = 16;

          drawHealthBar(ctx, x, y, currentHealth);
          ```

          With source maps, you get the best of both worlds: players can load your game quickly thanks to the minified bundle, and curious game developers can read and learn from your source in their dev tools.

          Minification and obfuscation won't stop people from reverse engineering your games, but they will stop curious minds from learning from you. So embrace the open ethos of the web and enable source maps for your games!

          To make it personal for a moment: my own interest in building web games started when I clicked `View Source` on [A Dark Room](https://adarkroom.doublespeakgames.com/) way back in 2014 and realized that building a game like this was *maybe&#x2A; within reach. That game had readable, &#x2A;**unminified***, familiar-looking source code--and very little of it! You could be responsible for sparking a similar moment for the visitor that clicks `View Source` on your game someday.
        </details>
      </>
    </slot>
  </Scrollycoding>

  <RuntimeOnly runtime="bun">
    <h4>
      Update <code>package.json</code>
    </h4>

    <Scrollycoding>
      <slot>
        <>        </>
      </slot>

      <slot path="steps">
        Vite's default scripts use Node.js instead of Bun as the runtime.
      </slot>

      <slot path="steps">
        Prepend `bunx --bun` to each script to make Vite run on Bun.
      </slot>
    </Scrollycoding>
  </RuntimeOnly>

  ## Start the dev server [#start-the-dev-server]

  First, start your local dev server so you can see changes live as you make them:

  <RuntimeOnly runtime="npm">
    ```bash
    npm run dev
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="bun">
    ```bash
    bun dev
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="deno">
    ```bash
    deno task dev
    ```
  </RuntimeOnly>

  Then open the URL displayed in your terminal, which will probably be like [`http://localhost:5173`](http://localhost:5173). You'll just see a blank page for now. If you open the browser console, you should see the log you added in `main.ts`.

  ## Add some starter code [#add-some-starter-code]

  ### Update `main.ts` [#update-maints]

  <Scrollycoding>
    <slot>
      <>      </>
    </slot>

    <slot path="steps">
      Start from your placeholder:
    </slot>

    <slot path="steps">
      Create a new `canvas` element and attach it to `document.body`.
    </slot>

    <slot path="steps">
      Read the `width` and `height` of the canvas so we can fill it. `getBoundingClientRect` returns an object with position-related properties.
    </slot>

    <slot path="steps">
      Fill the canvas with a blue rectangle. We'll cover this more in Chapter 2.
    </slot>
  </Scrollycoding>

  You should now see a blue rectangle in the top-left corner, which is the canvas at its default size. Next, we'll expand it to fill the entire window.

  ### Update `index.html` [#update-indexhtml]

  <Scrollycoding>
    <slot>
      <>      </>
    </slot>

    <slot path="steps">
      Start from Vite's generated HTML.
    </slot>

    <slot path="steps">
      Add an inline `<style>`. This stretches the canvas to fill available space and ensures there won't be scrollbars.
    </slot>
  </Scrollycoding>

  If you see a full-page blue rectangle, you have successfully initialized all the tooling you'll need to get started building web games.

  Finally, make sure everything builds successfully for production:

  <RuntimeOnly runtime="npm">
    ```bash
    npm run build
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="bun">
    ```bash
    bun run build
    ```
  </RuntimeOnly>

  <RuntimeOnly runtime="deno">
    ```bash
    deno task build
    ```
  </RuntimeOnly>

  ## Up next [#up-next]

  We'll cover painting to the canvas using `CanvasRenderingContext2D` in [chapter 2 →](/guides/handmade-web-games/intro-to-canvasrenderingcontext2d).
</RuntimeProvider>
