Guides
Sandwich Layering

Sandwich Layering

In overlay mode the canvas covers your content. Usually that is the point — but some elements must stay on top and stay interactive: navigation, modals, tooltips, a cursor-follower, a cookie banner.

Sandwich lifts those elements out of the flow into a fixed layer above the canvas, leaving an invisible placeholder behind so the page layout does not shift.

┌─────────────────────────────┐
│  front layer   z-index 9999 │  ← data-mirage-sandwich="front"
├─────────────────────────────┤
│  mid layer     z-index 9998 │  ← optional, via midLayerElement
├─────────────────────────────┤
│  Mirage canvas              │
├─────────────────────────────┤
│  original DOM               │
└─────────────────────────────┘

Usage

Sandwich is enabled by default. Mark the elements you want on top:

<nav data-mirage-sandwich="front">
  <a href="/docs">Docs</a>
  <a href="/api">API</a>
</nav>

That is the whole setup.

How it works

Layers are created

Two divs are appended to document.body: #sand-layer-mid (z-index 9998) and #sand-layer-front (z-index 9999). Both are position: fixed; inset: 0; pointer-events: none.

Front elements are hijacked

For each match, Sandwich measures the element, inserts a .sand-placeholder with the same box and margins at its original position, then moves the real element into the front layer with position: fixed and pointer-events: auto.

Positions sync every frame

On onBeforeRender, all placeholder rects are read in one batch, then all element positions are written in a second batch — reads and writes separated to avoid layout thrashing.

Custom selector

new Mirage(target, {
  sandwich: { frontSelector: ".hud, [data-floating]" },
});

Any valid querySelectorAll selector works. The default is [data-mirage-sandwich='front'].

Disabling

new Mirage(target, { sandwich: false });

Turn it off if you already manage stacking yourself, or if appending elements to document.body conflicts with your framework's portal system.

Standalone use

@mirage-engine/sandwich works without Mirage:

import { SandwichRenderer } from "@mirage-engine/sandwich";
 
const sandwich = new SandwichRenderer({
  frontSelector: ".floating",
  midLayerElement: document.querySelector("#my-canvas") as HTMLElement,
});
 
sandwich.init();

midLayerElement is moved into the mid layer and given pointer-events: auto — use it to place your own canvas between the page and the front layer.

Caveats

⚠️

Hijacked elements are physically moved in the DOM. If your framework owns that subtree (React reconciliation, Vue patching), it may try to move it back or crash on unmount. Prefer elements the framework treats as stable, or render them into a portal you control and leave Sandwich disabled.

LimitationDetail
CSS inheritance breaksThe element moves out of its original ancestor chain, so inherited styles and descendant selectors scoped to it stop applying
Only matched at init()Elements added later are not picked up; call init() again or manage them yourself
Placeholder is visibility: hiddenIt still occupies space, which is the point
position: fixed is forcedSticky behaviour inside the front layer will not work

Debugging

console.log(document.querySelector("#sand-layer-front")?.children);
console.log(document.querySelectorAll(".sand-placeholder").length);

If a front element drifts while scrolling, its placeholder was probably removed or hidden with display: none — the sync reads the placeholder's rect, so the placeholder must stay in layout.


Mirage Engine — MIT Licensed © 2026 dltldn333