Get Started
Quick Start

Quick Start

Mark the subtree you want to mirror

Mirage mirrors one element and everything inside it. Give it an id.

<div id="target">
  <h1>Hello, Mirage</h1>
  <p>This paragraph becomes a textured plane in WebGL.</p>
  <button>Still a real, clickable button</button>
</div>

The target must have a parent element — Mirage mounts its canvas as the parent's first child. document.body works; a detached node does not.

Create the engine and start it

import { Mirage } from "mirage-engine";
 
const target = document.querySelector("#target") as HTMLElement;
 
const mirage = new Mirage(target, {
  mode: "overlay",
  quality: "medium",
});
 
await mirage.start();

start() is async because it boots the WebAssembly module on first call. Always await it (or chain .then()) before you touch meshes.

Hide the original DOM (optional)

In overlay mode the WebGL copy is painted on top of the live DOM, so by default you see both. Add data-mirage-dom="hide" to fade the original out while keeping it interactive and accessible.

<div id="target" data-mirage-dom="hide">…</div>
⚠️

This sets opacity: 0, not display: none — the element still occupies layout, still receives clicks and is still read by screen readers. That is deliberate: Mirage needs the real layout box to mirror it.

Clean up

mirage.stop();    // pause the render loop, keep the scene
mirage.destroy(); // stop, dispose the renderer and remove the canvas

Full example

import { Mirage } from "mirage-engine";
 
const target = document.querySelector("#target") as HTMLElement;
 
const mirage = new Mirage(target, {
  mode: "overlay",        // "overlay" (default) | "duplicate"
  canvasSize: "viewport", // "viewport" (default) | "document"
  quality: "medium",      // "low" | "medium" | "high" | number
  resizeDebounce: { delay: 150 },
});
 
await mirage.start();
 
// Pause when the tab is hidden
document.addEventListener("visibilitychange", () => {
  document.hidden ? mirage.stop() : mirage.start();
});
 
// Tear down on unmount
window.addEventListener("beforeunload", () => mirage.destroy());

Duplicate mode

overlay covers the original in place. duplicate renders the copy into a different container instead, so you can show the same subtree twice.

const target = document.querySelector("#target") as HTMLElement;
const container = document.querySelector("#preview") as HTMLElement;
 
const mirage = new Mirage(target, {
  mode: "duplicate",
  container, // only valid in duplicate mode
});
 
await mirage.start();

See Rendering Modes for the full comparison.

Common mistakes

SymptomCauseFix
Cannot find a container throwsTarget has no parent, or duplicate mode without a mounted parentAttach the target to the document first
Nothing rendersstart() not awaited, or target has zero width/heightawait mirage.start(); give the target a layout box
You see the content twiceoverlay mode with the original still visibleAdd data-mirage-dom="hide"
Text looks blurryquality too low for the deviceRaise to "high", or pass a number like 3

Where to go next


Mirage Engine — MIT Licensed © 2026 dltldn333