Engineering Journal
Schema Editor
Schema Editor

A Layer Panel Is Not a File Browser. It Is a Live Mirror of the Scene Graph.

2026-06-04

TLDR

Layer panels built as separate, secondary data models (storing their own arrays of layer objects, folders, and z-index ordering) inevitably desynchronize from the actual render tree. The DOM itself is the authoritative scene graph. Treating the layer panel as a pure, zero-state projection generated directly from SVG DOM node order eliminates all layer sync bugs, simplifies undo/redo, and ensures z-index parity.
Architectural PatternData Storage ModelUndo / Redo HandlingDesynchronization Risk
Secondary Panel ModelArray of layer state objectsComplex custom state snapshottingHigh (Fails on import/undo)
DOM-Derived Pure ViewDOM attributes (data-layer-name)Automatic (Rebuilds on DOM change)Zero (Guaranteed parity)

Problem statement: the flaws of dual-model architecture

In vector design applications, developers often treat the layer panel like a file explorer UI, maintaining an array of layer objects ({ id, name, zIndex, children }) separate from the SVG canvas elements.

This dual-model architecture requires intercepting every canvas mutation path (add, delete, move, group, paste, import, undo/redo) to keep the secondary layer model in sync with the DOM. Miss a single event handler, and the layer panel displays stale or orphaned entries.


Technical failure mode: Z-order & mutation desynchronization

  1. Z-Order Inversion: SVG rendering order is determined strictly by DOM child node sequence. When a secondary panel model tracks z-index independently, reordering entries in the layer panel can easily diverge from true DOM painting sequence.
  2. Undo/Redo Breakdown: Reverting a canvas state via DOM snapshot restoration updates the canvas tree immediately, but leaves the secondary layer panel model holding pre-undo state.

The fix & architecture: deriving panels directly from the DOM

Treat the DOM as the single source of truth. Store layer metadata directly on SVG elements as data-* attributes and rebuild the layer UI by walking DOM child nodes:

// REFACTORED: Zero-state layer panel builder
function buildLayerPanel(contentRoot, panelContainer) {
  panelContainer.innerHTML = ''; // Fast reset (<1ms for typical diagrams)

// Walk SVG children in DOM order — DOM order IS Z-order! Array.from(contentRoot.children) .filter(el => !el.dataset.system) // Filter out system UI elements .forEach(el => { const row = createLayerRow(el); panelContainer.appendChild(row); }); }

function createLayerRow(el) { const row = document.createElement('div'); row.className = 'layer-row'; row.textContent = el.getAttribute('data-layer-name') || el.id;

// Toggle visibility directly on DOM node const hideBtn = document.createElement('button'); hideBtn.onclick = () => { const isHidden = el.dataset.hidden === 'true'; el.dataset.hidden = String(!isHidden); el.style.display = isHidden ? '' : 'none'; buildLayerPanel(contentRoot, panelContainer); // Rebuild reads DOM state! };

return row; }

Reordering DOM nodes directly

Moving a layer in the panel executes native DOM node reordering (insertBefore), eliminating the need for a secondary z-index array:
function moveElementBefore(movedEl, targetEl, contentRoot) {
  contentRoot.insertBefore(movedEl, targetEl);
  buildLayerPanel(contentRoot, panelContainer); // Rebuild reflects DOM order
}
Rule of thumb: Never maintain a secondary array for layer panel state. Store visibility and layer names as data-* attributes directly on DOM nodes, and generate layer UI rows by walking the DOM tree.
Read this post in the full Engineering Journal →