PixiJS in a single HTML file, no install and no build step

PixiJS is a 2D graphics library for the web. In a plain HTML page it takes one script tag and an async setup call, and then you can draw shapes, text and animation on a canvas.

PixiJS is a 2D graphics library for the web. This guide is about using it in one plain HTML file: a script tag loads it, an async function sets it up, and you draw on a canvas. Nothing to install and nothing to build.

Here is the smallest useful page. A square turns, a title sits on top, and the line underneath says which PixiJS version and renderer are running.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PixiJS in one HTML file</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  #stage { height: 260px; }          /* the canvas will fill this box */
  #stage canvas { display: block; }  /* no stray gap under the canvas */
  #info { margin: 0; padding: 10px 14px; font-size: 14px; }
</style>
</head>
<body>
<div id="stage"></div>
<p id="info">Loading PixiJS...</p>

<!-- 1. the library, pinned to one version. It creates a global called PIXI -->
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.21.0/dist/pixi.min.js"></script>
<script>
  (async () => {
    const holder = document.getElementById('stage');
    const app = new PIXI.Application();

    // 2. init() is async: wait for it before you touch app.canvas
    await app.init({
      resizeTo: holder,              // canvas follows the size of this element
      background: '#1d2330',
      antialias: true,
      resolution: window.devicePixelRatio,  // sharp on high-density screens
      autoDensity: true,
    });
    holder.appendChild(app.canvas);

    // 3. add things to app.stage
    const box = new PIXI.Graphics().roundRect(-50, -50, 100, 100, 18).fill(0x4f8cff);
    const label = new PIXI.Text({ text: 'Hello, Pixi', style: { fill: '#ffffff', fontSize: 22 }, anchor: 0.5 });
    app.stage.addChild(box, label);

    function center() {
      box.position.set(app.screen.width / 2, app.screen.height / 2 + 14);
      label.position.set(app.screen.width / 2, 30);
    }
    center();
    app.renderer.on('resize', center);

    // 4. the ticker runs once per frame
    app.ticker.add((time) => { box.rotation += 0.02 * time.deltaTime; });

    document.getElementById('info').textContent =
      'PixiJS ' + PIXI.VERSION + ' - renderer: ' + app.renderer.name;
  })();
</script>
</body>
</html>
The whole page: script tag, async setup, a rounded square, a text label and a ticker. Edit the code and the example reruns.

The examples pin version 8.21.0, the latest on npm when this page was written. All of them load the library from jsDelivr, one of the CDN hosts NOS allows.

What PixiJS is, and what lands in the page

PixiJS draws with WebGL and WebGPU renderers and is released under the MIT license. It keeps a tree of objects, called the stage, and redraws that tree on every frame while its ticker runs.

PixiJS draws into one canvas element. The shapes and text are pixels inside it.
PixiJS draws into one canvas element. The shapes and text are pixels inside it.

That has one consequence for HTML: the page holds a single <canvas>. The shapes are not elements, and the Pixi text is not page text.

For labels that must be selectable or read aloud, put normal HTML next to the canvas, as the examples do for scores. For lower-level drawing without a library, see WebGL in HTML.

The five steps every file needs

Every PixiJS page, however big, starts the same way.

The five steps of a PixiJS page, top to bottom.
The five steps of a PixiJS page, top to bottom.
  1. Load the library. A script tag for pixi.min.js. It creates a global called PIXI.
  2. Add a box with a height. A <div> with a CSS height for the canvas to fill.
  3. Create and initialize. Inside an async function: new PIXI.Application(), then await app.init(...).
  4. Add the canvas. Append app.canvas to your box.
  5. Add things and a loop. Put objects on app.stage and animate them in app.ticker.

Put the library tag above your own script. Scripts run top to bottom, so code that sits above it cannot see PIXI yet.

Write the version into the address, as in pixi.js@8.21.0. jsDelivr loads the newest release if you leave the version out, and its README says that is not recommended for production use.

In an npm project. The same code works in a bundler project, with an import in place of the global. This part needs Node.js and does not run in a plain HTML file:

npm create pixi.js@latest
import { Application, Graphics } from 'pixi.js';

A bare import like this fails in a plain HTML page, because the browser cannot resolve a module name without an import map. In a single file, use the script tag and the global.

Why PixiJS 8 needs await app.init()

PixiJS 8 sets itself up asynchronously, since it now has a WebGPU renderer next to the WebGL one. Options go into init() instead of the constructor, and the canvas is ready only after init() finishes.

Options and the canvas belong after init in PixiJS 8. Version 7 code fails on the first canvas read.
Options and the canvas belong after init in PixiJS 8. Version 7 code fails on the first canvas read.

Older tutorials write new Application({ ... }) and read app.view. In 8.21.0 that code throws a TypeError as soon as it reads the canvas, because nothing has been initialized yet. The fix is await app.init({ ... }) and app.canvas.

A classic script cannot use await at the top level, only inside an async function or in a module script. So wrap the setup in an async function that runs right away:

(async () => {
  const app = new PIXI.Application();
  await app.init({ resizeTo: holder, background: '#1d2330' });
  holder.appendChild(app.canvas);
  // add shapes here
})();

Draw, label and animate

Shapes come from PIXI.Graphics. Describe the shape, then fill it:

const box = new PIXI.Graphics().roundRect(-50, -50, 100, 100, 18).fill(0x4f8cff);
const dot = new PIXI.Graphics().circle(0, 0, 30).fill('tomato');

Drawing around the point 0, 0 makes a shape turn around its own middle. Position it with box.position.set(x, y).

Text uses PIXI.Text, which renders with the browser's own text engine and turns the result into a texture. Change it occasionally, not on every frame.

Animation goes in the ticker. It runs once per frame and passes a time object. Multiply each step by deltaTime and the motion no longer depends on the frame rate. The ticker is driven by requestAnimationFrame, covered in requestAnimationFrame.

app.ticker.add((time) => {
  box.rotation += 0.02 * time.deltaTime;
});

Images need care. PIXI.Assets.load() can fetch a picture from an address, but NOS blocks images from other sites. In a shared single file, draw with Graphics instead.

Respond to taps and clicks

Shapes ignore the pointer until you opt in. Every object starts with eventMode set to passive. Set it to static and the object emits events and is hit tested. Then on('pointerdown', fn) works for a mouse, a pen and a finger.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PixiJS events: eventMode</title>
<style>
  body { margin: 0; padding: 10px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; gap: 10px; }
  .col { flex: 1; min-width: 0; }
  h3 { margin: 0 0 6px; font-size: 13px; font-family: ui-monospace, Consolas, monospace; }
  .col.bad h3 { color: #9a3412; }
  .col.good h3 { color: #0f5132; }
  .stage { height: 190px; border-radius: 10px; overflow: hidden; }
  .stage canvas { display: block; }
  p { margin: 6px 0 0; font-size: 14px; }
  .note { font-size: 12.5px; color: #4b5563; margin: 10px 0 0; }
</style>
</head>
<body>
<div class="row">
  <div class="col bad">
    <h3>default eventMode</h3>
    <div class="stage" id="stageA"></div>
    <p>Taps: <b id="countA">0</b></p>
  </div>
  <div class="col good">
    <h3>eventMode = 'static'</h3>
    <div class="stage" id="stageB"></div>
    <p>Taps: <b id="countB">0</b></p>
  </div>
</div>
<p class="note">Both circles have the same pointerdown listener. Tap each one.</p>

<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.21.0/dist/pixi.min.js"></script>
<script>
  async function makeStage(holderId, countId, color, mode) {
    const holder = document.getElementById(holderId);
    const out = document.getElementById(countId);
    const app = new PIXI.Application();
    await app.init({ resizeTo: holder, background: '#1d2330', antialias: true,
                     resolution: window.devicePixelRatio, autoDensity: true });
    holder.appendChild(app.canvas);

    const dot = new PIXI.Graphics().circle(0, 0, 44).fill(color);
    dot.position.set(app.screen.width / 2, app.screen.height / 2);
    if (mode) dot.eventMode = mode;   // without this line the object ignores the pointer
    dot.cursor = 'pointer';
    let n = 0;
    dot.on('pointerdown', () => {
      out.textContent = ++n;
      dot.scale.set(1.25);
    });
    dot.on('pointerup', () => dot.scale.set(1));
    dot.on('pointerupoutside', () => dot.scale.set(1));
    app.stage.addChild(dot);
  }
  makeStage('stageA', 'countA', 0xf59e0b, null);
  makeStage('stageB', 'countB', 0x22c55e, 'static');
</script>
</body>
</html>
Both circles have the same listener. Only the one with eventMode set to static counts taps.

Add cursor = 'pointer' for a hand cursor over the shape.

In version 8, a plain pointermove fires only while the pointer is over an object. To follow the pointer everywhere, for example while dragging, listen for globalpointermove instead. For the DOM side of pointer input, see pointer events.

Keep it sharp and sized right on phones

Two options decide whether the canvas looks right. resizeTo makes the canvas follow an element or the window.

resolution sets pixel density, and its default is 1. On a screen where devicePixelRatio is 2, a canvas left at 1 has one backing pixel per CSS pixel, so it holds fewer pixels than the screen can show.

await app.init({
  resizeTo: holder,
  resolution: window.devicePixelRatio,
  autoDensity: true,   // the canvas CSS size stays the same
});

In a window 390 pixels wide with a pixel ratio of 2, this gives a canvas 780 pixels wide inside a 390-pixel box. The page also needs the viewport meta tag, and blurry canvas fixes cover the plain-canvas version of this problem.

Give the box a CSS height too. With no height on the box, the test canvas came out 600 pixels tall, which is the default height, not the size of the box.

A finished example: pop the bubbles

This page puts the pieces together. Bubbles are Graphics objects, each with its own speed. Tapping one raises the score and destroys it. The ticker moves them up and removes the ones that leave the top.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pop the bubbles - PixiJS</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  header { display: flex; align-items: center; gap: 14px; padding: 10px 14px; font-size: 15px; }
  header span { white-space: nowrap; }
  button { margin-left: auto; font: inherit; padding: 6px 14px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; }
  #stage { height: 300px; touch-action: none; }  /* a finger plays the game instead of scrolling the page */
  #stage canvas { display: block; }
  #msg { padding: 8px 14px; font-size: 14px; min-height: 20px; }
</style>
</head>
<body>
<header>
  <span>Score: <b id="score">0</b></span>
  <span>Time: <b id="time">30</b></span>
  <button id="restart" type="button">Restart</button>
</header>
<div id="stage"></div>
<div id="msg">Tap the bubbles before they float away.</div>

<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.21.0/dist/pixi.min.js"></script>
<script>
  const COLORS = [0x4f8cff, 0x22c55e, 0xf59e0b, 0xec4899];
  const holder = document.getElementById('stage');
  const bubbles = [];
  let app, score = 0, timeLeft = 30, spawnIn = 0;

  function addBubble() {
    const r = 18 + Math.random() * 14;
    const b = new PIXI.Graphics()
      .circle(0, 0, r).fill({ color: COLORS[Math.floor(Math.random() * COLORS.length)], alpha: 0.85 })
      .stroke({ width: 2, color: 0xffffff });
    b.position.set(r + Math.random() * (app.screen.width - 2 * r), app.screen.height + r);
    b.speed = 1 + Math.random() * 1.5;
    b.eventMode = 'static';
    b.cursor = 'pointer';
    b.on('pointerdown', () => {
      score++;
      document.getElementById('score').textContent = score;
      remove(b);
    });
    app.stage.addChild(b);
    bubbles.push(b);
  }

  function remove(b) {
    bubbles.splice(bubbles.indexOf(b), 1);
    b.destroy();   // frees the graphics and removes it from the stage
  }

  function reset() {
    while (bubbles.length) remove(bubbles[0]);
    score = 0; timeLeft = 30; spawnIn = 0;
    document.getElementById('score').textContent = 0;
    document.getElementById('msg').textContent = 'Tap the bubbles before they float away.';
  }

  async function start() {
    app = new PIXI.Application();
    await app.init({ resizeTo: holder, background: '#1d2330', antialias: true,
                     resolution: window.devicePixelRatio, autoDensity: true });
    holder.appendChild(app.canvas);

    app.ticker.add((t) => {
      if (timeLeft > 0) {
        timeLeft = Math.max(0, timeLeft - t.elapsedMS / 1000);
        document.getElementById('time').textContent = Math.ceil(timeLeft);
        spawnIn -= t.elapsedMS;
        if (spawnIn <= 0) { addBubble(); spawnIn = 350; }
        if (timeLeft === 0) document.getElementById('msg').textContent = "Time's up. Score: " + score;
      }
      for (let i = bubbles.length - 1; i >= 0; i--) {
        bubbles[i].y -= bubbles[i].speed * t.deltaTime;
        if (bubbles[i].y < -40) remove(bubbles[i]);   // floated off the top
      }
    });
  }

  document.getElementById('restart').addEventListener('click', reset);
  start();
</script>
</body>
</html>
Tap the bubbles for thirty seconds. Score and time are normal HTML, and the bubbles are drawn by PixiJS.
  • Many objects: keep them in an array, and loop backwards when you remove some during the loop.
  • Clean up: call destroy() on an object you are done with, so memory does not pile up.
  • Touch: touch-action: none on the stage box lets a finger play the game instead of scrolling the page.

More ideas for this kind of page are in single-file HTML games.

When it does not work

What you see Cause Fix
PIXI is not defined Your code runs before the library has loaded Put the library script tag first
TypeError reading canvas Canvas read before init, or options in the constructor Call await app.init() first, then read app.canvas
await is only valid in async functions A classic script has no top-level await Wrap the setup in an async function
Failed to resolve module specifier A bare import of pixi.js in a plain page Use the script tag and the PIXI global
Canvas is 600 pixels tall The box has no CSS height Set a height on the box
Canvas looks soft on a phone resolution is left at 1 Set resolution and autoDensity
A shape ignores taps eventMode is still passive Set eventMode to static
Dragging loses the object pointermove fires only over an object Listen for globalpointermove
A picture from another site is missing in NOS NOS blocks outside images Draw the shape with Graphics

For other scripts that stay silent, see JavaScript not working in HTML.

A PixiJS page is easier to show than to describe. A screenshot cannot be tapped, and an .html attachment may open as plain code, or not at all, on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written, the PixiJS script from jsDelivr loads and your scripts run, so the people you send it to can tap the bubbles themselves, with no account. If you change the code later, the same link shows the new version.

Questions people ask

Do I need npm or a build step to use PixiJS?

Not for a single HTML file. Load pixi.min.js with a script tag and use the global PIXI object. The project's own setup commands, npm create pixi.js@latest and npm install pixi.js, are for projects that use a bundler.

Which script do I put in the script tag?

The examples here load pixi.min.js from cdn.jsdelivr.net with the version written into the address, 8.21.0. That was the latest version on npm on 1 October 2026. Pinning the version keeps the page the same even after a new release.

Is PixiJS free to use?

Yes. PixiJS is open source and released under the MIT license.

Does PixiJS use WebGL or WebGPU?

It has renderers for both. The Application option called preference defaults to webgl, and you can ask for webgpu instead. The first example prints which renderer it got.

Why does my PixiJS code fail with a TypeError about canvas?

PixiJS 8 sets itself up asynchronously. The canvas exists only after await app.init() finishes, and options go into init(), not into the constructor. Code written for version 7 reads app.view too early and fails.

Keep reading