Babylon.js in a single HTML file

Babylon.js is a JavaScript library that draws 3D scenes on a canvas. Add one script tag, a canvas and a few lines of code, and a spinning 3D shape runs from a plain .html file.

Babylon.js is a JavaScript library for 3D graphics in the browser. It draws into a <canvas> element, and you load it with a plain script tag.

This guide is the shortest path from an empty file to a 3D scene you can orbit with a mouse or a finger.

Try it first. Drag the cube to turn the camera, and use the mouse wheel to zoom.

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>Babylon.js in one HTML file</title>
<style>
  html, body { margin: 0; height: 100%; overflow: hidden; background: #eef1f6; font-family: system-ui, sans-serif; }
  /* the canvas needs a real size, or nothing is drawn */
  #stage { display: block; width: 100%; height: 100%; outline: none; }
  #msg { position: absolute; left: 10px; bottom: 8px; font-size: 13px; color: #374151; pointer-events: none; }
</style>
</head>
<body>
<canvas id="stage"></canvas>
<div id="msg">Drag to orbit, mouse wheel to zoom.</div>

<!-- load the library BEFORE the script that uses it; pin the version -->
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
  const canvas = document.getElementById('stage');
  const engine = new BABYLON.Engine(canvas, true);   // true = antialiasing
  const scene = new BABYLON.Scene(engine);
  scene.clearColor = new BABYLON.Color4(0.93, 0.95, 0.97, 1);

  // camera that orbits a target: name, alpha, beta, radius, target
  const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 3, Math.PI / 3, 6, BABYLON.Vector3.Zero(), scene);
  camera.attachControl(canvas, false);               // mouse, touch and wheel control
  camera.lowerRadiusLimit = 3;                       // keep zoom from passing through the cube
  camera.upperRadiusLimit = 14;

  new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.3, 1, 0.2), scene);

  const box = BABYLON.MeshBuilder.CreateBox('box', { size: 2 }, scene);
  const mat = new BABYLON.StandardMaterial('mat', scene);
  mat.diffuseColor = new BABYLON.Color3(0.15, 0.45, 0.9);
  box.material = mat;

  scene.onBeforeRenderObservable.add(() => { box.rotation.y += 0.01; });

  engine.runRenderLoop(() => scene.render());
  window.addEventListener('resize', () => engine.resize());
</script>
</body>
</html>
A spinning cube in one HTML file. The library comes from a script tag, and the page has one short script of your own.

The whole page is the example. Copy it into an empty .html file and it runs, as long as the browser can reach the CDN.

What the file needs

A Babylon.js page has three parts, in this order.

The three parts of the file in order: a sized canvas, the library script tag, then your own script.
The three parts of the file in order: a sized canvas, the library script tag, then your own script.

The canvas is where the picture appears. The library script creates the global name BABYLON. Your script uses that name, so it has to come after the library tag.

The five things every scene has

Every Babylon.js scene is built from the same short list. Nothing shows up if one of them is missing.

An engine holds a scene. The scene holds a camera, a light and a mesh. The render loop draws the scene every frame.
An engine holds a scene. The scene holds a camera, a light and a mesh. The render loop draws the scene every frame.
  1. Engine – new BABYLON.Engine(canvas, true) connects to the canvas. The second argument turns on antialiasing.
  2. Scene – new BABYLON.Scene(engine) is the container for everything you can see.
  3. Camera – an ArcRotateCamera orbits a target point. attachControl lets the mouse, a finger and the wheel move it.
  4. Light – a HemisphericLight gives the shapes something to reflect.
  5. Mesh – MeshBuilder.CreateBox and its siblings make a ready-made shape.

Then engine.runRenderLoop calls your function every frame. To animate, change a value inside onBeforeRenderObservable, such as the box rotation, and the next frame shows it.

Load the library before your code

Browsers run script tags from top to bottom. If your code comes first, the library has not loaded yet and the console says the name does not exist.

Left: your script above the library tag fails with BABYLON is not defined. Right: the library first, so the scene appears.
Left: your script above the library tag fails with BABYLON is not defined. Right: the library first, so the scene appears.

I checked this in a browser: with the order reversed, the page reports BABYLON is not defined. Nothing is drawn.

Pin the version and mind the size

The examples load this file:

<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>

The @9.28.0 part pins the release. Without it, the same URL answers with the newest release, so a page you wrote today can change when a new version ships. unpkg serves the same file under the same pinned name.

The file is 8.6 MB before compression (8,592,001 bytes), so the first load takes a moment. The Babylon.js README adds that its own CDN is meant for learning and small experiments, and that a finished app should serve the library from its own hosting.

Break it on purpose

Blank pages usually trace back to a few causes. Switch each one on and read what happens.

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>Break a Babylon.js scene on purpose</title>
<style>
  html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; background: #eef1f6; }
  body { display: flex; flex-direction: column; }
  .bar { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px; background: #fff; border-bottom: 1px solid #dde1e8; }
  .bar button { font: inherit; font-size: 13px; padding: 6px 10px; border: 1px solid #b9c0cc; border-radius: 8px; background: #f7f8fa; cursor: pointer; }
  .bar button[aria-pressed="true"] { background: #fde8d4; border-color: #e08a3c; }
  #note { padding: 6px 10px; font-size: 13px; color: #374151; min-height: 18px; background: #fff; }
  .wrap { position: relative; flex: 1; min-height: 0; }
  #stage { display: block; width: 100%; height: 100%; outline: none; }
</style>
</head>
<body>
<div class="bar">
  <button id="bLight" aria-pressed="false">No light</button>
  <button id="bCam" aria-pressed="false">No camera control</button>
  <button id="bSize" aria-pressed="false">Canvas 0px tall</button>
</div>
<div id="note">Everything works. Press a button to see what each mistake looks like.</div>
<div class="wrap"><canvas id="stage"></canvas></div>

<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
  const canvas = document.getElementById('stage');
  const note = document.getElementById('note');
  const engine = new BABYLON.Engine(canvas, true);
  const scene = new BABYLON.Scene(engine);
  scene.clearColor = new BABYLON.Color4(0.93, 0.95, 0.97, 1);

  const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 3, Math.PI / 3, 7, BABYLON.Vector3.Zero(), scene);
  camera.attachControl(canvas, false);
  camera.lowerRadiusLimit = 4;
  camera.upperRadiusLimit = 14;
  const light = new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.3, 1, 0.2), scene);

  const ball = BABYLON.MeshBuilder.CreateSphere('ball', { diameter: 2.4 }, scene);
  const mat = new BABYLON.StandardMaterial('mat', scene);
  mat.diffuseColor = new BABYLON.Color3(0.9, 0.35, 0.2);
  ball.material = mat;
  scene.onBeforeRenderObservable.add(() => { ball.rotation.y += 0.01; });

  engine.runRenderLoop(() => scene.render());
  window.addEventListener('resize', () => engine.resize());

  const state = { light: true, cam: true, size: true };
  function say() {
    const msgs = [];
    if (!state.light) msgs.push('No light: a lit material has nothing to reflect, so the sphere is dark.');
    if (!state.cam) msgs.push('Camera not attached: dragging does nothing.');
    if (!state.size) msgs.push('Canvas has no height: nothing can be drawn.');
    note.textContent = msgs.length ? msgs.join(' ') : 'Everything works. Press a button to see what each mistake looks like.';
  }
  function flip(btn, key, apply) {
    btn.addEventListener('click', () => {
      state[key] = !state[key];
      btn.setAttribute('aria-pressed', String(!state[key]));
      apply(state[key]);
      say();
    });
  }
  flip(document.getElementById('bLight'), 'light', (on) => light.setEnabled(on));
  flip(document.getElementById('bCam'), 'cam', (on) => {
    if (on) camera.attachControl(canvas, false); else camera.detachControl();
  });
  flip(document.getElementById('bSize'), 'size', (on) => {
    canvas.style.height = on ? '100%' : '0px';
    engine.resize();   // tell Babylon the canvas size changed
  });
</script>
</body>
</html>
Three buttons that each break the scene: no light, no camera control, and a canvas with no height.

A canvas without a CSS size falls back to 300 by 150 pixels, a default that MDN documents.

A canvas with a parent of zero height has nothing to draw into. Give html, body and the canvas real sizes, and call engine.resize() when the window changes.

On a phone, a camera attached with false calls preventDefault on the events it handles. A full-screen canvas then orbits under a finger instead of scrolling the page.

Leave some page outside the canvas if people need to scroll. Add the viewport meta tag so the page is sized for a phone.

A finished example: pick and colour

Interaction is where a 3D scene starts to feel like an app. This version lets people tap a shape and recolour it with ordinary HTML buttons.

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>Pick and colour 3D shapes</title>
<style>
  html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; background: #eef1f6; }
  body { display: flex; flex-direction: column; }
  .bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 10px; background: #fff; border-bottom: 1px solid #dde1e8; font-size: 13px; }
  .sw { width: 28px; height: 28px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px #b9c0cc; cursor: pointer; padding: 0; }
  .bar button.t { font: inherit; font-size: 13px; padding: 5px 10px; border: 1px solid #b9c0cc; border-radius: 8px; background: #f7f8fa; cursor: pointer; }
  #pick { color: #1d2330; font-weight: 600; min-width: 130px; }
  .wrap { flex: 1; min-height: 0; }
  #stage { display: block; width: 100%; height: 100%; outline: none; }
  #fail { display: none; padding: 20px; color: #9a3412; }
</style>
</head>
<body>
<div class="bar">
  <span id="pick">Tap a shape</span>
  <button class="sw" data-c="#e4572e" style="background:#e4572e" aria-label="Orange"></button>
  <button class="sw" data-c="#2e86e4" style="background:#2e86e4" aria-label="Blue"></button>
  <button class="sw" data-c="#2fb26a" style="background:#2fb26a" aria-label="Green"></button>
  <button class="sw" data-c="#f2c230" style="background:#f2c230" aria-label="Yellow"></button>
  <button class="t" id="spin" aria-pressed="true">Spin: on</button>
</div>
<div class="wrap"><canvas id="stage"></canvas></div>
<div id="fail">This browser could not start WebGL, so the 3D scene cannot be drawn.</div>

<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
  const canvas = document.getElementById('stage');
  const label = document.getElementById('pick');
  let engine;
  try {
    engine = new BABYLON.Engine(canvas, true);
  } catch (err) {
    document.querySelector('.wrap').style.display = 'none';
    document.getElementById('fail').style.display = 'block';
  }

  if (engine) {
    const scene = new BABYLON.Scene(engine);
    scene.clearColor = new BABYLON.Color4(0.93, 0.95, 0.97, 1);
    const cam = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.1, 9, new BABYLON.Vector3(0, 0.5, 0), scene);
    cam.attachControl(canvas, false);
    cam.lowerRadiusLimit = 4;
    cam.upperRadiusLimit = 24;
    if (canvas.clientWidth < 500) cam.radius = 17;   // narrow screen: step back to fit all three
    new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.3, 1, 0.2), scene);

    const shapes = [
      BABYLON.MeshBuilder.CreateBox('Cube', { size: 1.8 }, scene),
      BABYLON.MeshBuilder.CreateSphere('Sphere', { diameter: 2 }, scene),
      BABYLON.MeshBuilder.CreateTorusKnot('Knot', { radius: 0.7, tube: 0.25, radialSegments: 80 }, scene),
    ];
    shapes.forEach((m, i) => {
      m.position.x = (i - 1) * 3;
      m.material = new BABYLON.StandardMaterial('m' + i, scene);
      m.material.diffuseColor = BABYLON.Color3.FromHexString('#8a94a6');
    });

    const ground = BABYLON.MeshBuilder.CreateGround('ground', { width: 12, height: 6 }, scene);
    ground.position.y = -1.4;
    ground.material = new BABYLON.StandardMaterial('g', scene);
    ground.material.diffuseColor = new BABYLON.Color3(0.82, 0.85, 0.9);
    ground.isPickable = false;               // clicks pass through the floor

    let selected = null, spin = true;
    function select(mesh) {
      if (selected) selected.material.emissiveColor = BABYLON.Color3.Black();
      selected = mesh;
      if (selected) selected.material.emissiveColor = new BABYLON.Color3(0.12, 0.12, 0.12);
      label.textContent = selected ? 'Selected: ' + selected.name : 'Tap a shape';
    }

    // POINTERTAP fires for a tap or click, not at the end of an orbit drag
    scene.onPointerObservable.add((info) => {
      if (info.type !== BABYLON.PointerEventTypes.POINTERTAP) return;
      select(info.pickInfo.hit ? info.pickInfo.pickedMesh : null);
    });

    document.querySelectorAll('.sw').forEach((b) => b.addEventListener('click', () => {
      if (selected) selected.material.diffuseColor = BABYLON.Color3.FromHexString(b.dataset.c);
    }));
    const spinBtn = document.getElementById('spin');
    spinBtn.addEventListener('click', () => {
      spin = !spin;
      spinBtn.textContent = 'Spin: ' + (spin ? 'on' : 'off');
      spinBtn.setAttribute('aria-pressed', String(spin));
    });

    scene.onBeforeRenderObservable.add(() => {
      if (spin) shapes.forEach((m) => { m.rotation.y += 0.008; });
    });
    engine.runRenderLoop(() => scene.render());
    window.addEventListener('resize', () => engine.resize());
  }
</script>
</body>
</html>
Tap a shape to select it, then choose a colour. Dragging still orbits the camera, and a tap does not.
  • Tap detection: PointerEventTypes.POINTERTAP fires for a tap or click, and pickInfo.pickedMesh says which shape was hit. I checked that an orbit drag over a shape does not select it.
  • Floor ignored: ground.isPickable = false lets taps pass through the floor.
  • HTML controls: the colour buttons are normal buttons that change diffuseColor on the selected material.
  • No WebGL: the engine is created inside try, and a plain message shows if the browser cannot start WebGL.

Babylon.js is one route to 3D. For drawing a triangle with no library at all, see WebGL in HTML. For a cube made only of CSS, see CSS 3D cube.

When it does not work

What you see Cause Fix
Console: BABYLON is not defined Your script runs before the library tag Put the library tag first
Blank page, no error The canvas has no height Size html, body and the canvas in CSS
A small 300 by 150 picture No CSS size on the canvas Set width and height in CSS
The shape is black The scene has no light Add a HemisphericLight
The mouse does nothing The camera is not attached Call camera.attachControl(canvas, false)
The picture ignores window size The engine is never told Call engine.resize() on resize
Works today, changes later The URL has no version Pin it, such as @9.28.0
Blank on one device That browser cannot start WebGL Wrap the engine in try and show a message

A 3D page is hard to show in words, and a screenshot cannot be orbited. An .html attachment may open as plain code, or not at all, on a phone. Opening an HTML file on a phone covers why.

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 Babylon.js script loads from jsDelivr, and the scene runs, so anyone with the link can orbit and tap it without an 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 Babylon.js?

Not for a single HTML file. A script tag that loads babylon.js gives you one global name, BABYLON, and the rest is plain JavaScript. The npm package is for projects that use a bundler, and its own description recommends the @babylonjs/core package for new projects of that kind.

Is Babylon.js free to use?

It is open source under the Apache License 2.0, as listed on its GitHub repository and its npm package. Read the licence text itself if you plan to ship it inside a product.

Why is my 3D shape black?

A standard material needs light to show its colour. If the scene has no light, the shape renders dark. Add a HemisphericLight and the colour comes back. The second example above lets you switch the light off to see it.

Which file should I load, babylon.js or babylon.max.js?

In version 9.28.0 the jsDelivr file listing shows the same hash for both, so they hold the same code. Use babylon.js and pin the version in the URL.

Does it work on a phone?

The camera reads pointer input, so a finger drag orbits the scene and the demos here do that in a phone-sized browser. The library file is large, so the first load on a slow connection takes a moment.

Keep reading