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 independent data models (their own folder tree, their own z-order list) drift from the render tree the moment any canvas operation bypasses the panel. The fix is treating the panel as a pure view: derive everything from the render tree, make DOM order the z-order, and rebuild the panel on every relevant change.


The Problem Class

Every visual editor has some form of layer or object panel. It shows a list of objects in the diagram, lets users rename, reorder, show, hide, and group them. The implementation question: does the panel maintain its own data model, or is it derived from the render tree on demand?

This sounds like an implementation detail. It determines whether the panel is always correct or eventually inconsistent.

The Naive Approach

The natural design is a separate panel data model. You have objects in the diagram and you have a layer list in the panel. They stay in sync through events: when you add a shape, you add an entry to the layer list. When you delete a shape, you remove the entry. When you drag a layer entry to reorder, you reorder the render tree.

This pattern comes from file system UIs: Finder, VS Code's explorer, Figma's layers panel. It feels correct because these tools work this way. The difference is that file system UIs manage their own objects. A diagram's layer panel is a view of objects managed by the canvas.

Why It Breaks

Sync breaks when canvas operations do not notify the layer model. The canvas has many paths that modify the element set: paste, undo/redo, import, group, ungroup, analysis operations that add system elements. Each path must emit an event that triggers a panel rebuild.

Any path that is missed produces a desync. The panel shows stale data. Users see layer entries that no longer exist, or they cannot select elements because the panel entry points to a deleted object.

Z-order desync is subtler. If the panel maintains its own ordering separate from DOM order, the two can disagree. The user sees element A above element B on the canvas (DOM order: B before A in the tree, meaning A renders on top). The panel shows A below B. Dragging layers in the panel reorders the panel data but may not update the DOM, or updates the DOM in a different way than the panel intended.

Rename sync is a specific failure case: the panel entry stores a name property separate from the element's data-layer-name attribute. After a rename through the panel, the attribute is updated. If the attribute is also updated by some other path (import, undo), the panel name is stale.

The Better Model

The render tree is the source of truth. The panel is a view. Build the panel by walking the render tree directly:

function buildPanel(contentRoot, panelContainer) {
    panelContainer.innerHTML = '';
    // Walk SVG children in DOM order โ€” DOM order is z-order
    Array.from(contentRoot.children)
        .filter(el => !el.dataset.system) // skip system elements
        .forEach(el => {
            const row = buildRow(el);
            panelContainer.appendChild(row);
        });
}

DOM order is z-order by definition. The panel order exactly matches the render order the user sees. Reordering in the panel reorders the DOM:

function moveElementBefore(movedEl, targetEl, contentRoot) {
    contentRoot.insertBefore(movedEl, targetEl);
    // No separate model to update โ€” DOM is the model
    buildPanel(contentRoot, panelContainer);
}

The panel rebuilds after every operation that might change the element set. For a diagram with hundreds of elements, a panel rebuild takes under a millisecond. The cost is insignificant compared to the correctness guarantee.

Visibility and lock state live on the elements as data attributes:

function toggleVisibility(el) {
    const hidden = el.dataset.hidden === 'true';
    el.dataset.hidden = String(!hidden);
    el.style.display = hidden ? '' : 'none';
    buildPanel(contentRoot, panelContainer); // panel reads from DOM
}

No separate visibility store. The DOM is the store.

Tradeoffs

Rebuilding the panel on every change means no persistent panel state between rebuilds. Scroll position in the panel may jump if the panel is long. Expanded/collapsed group state needs to be preserved explicitly (store which group IDs are collapsed, restore after rebuild).

The panel rebuild approach also means undo/redo does not need special handling for the panel: restoring a canvas snapshot rebuilds the panel automatically because the DOM changed.

The main cost is that the pattern requires a fast, complete panel builder. If the panel has complex UI (drag handles, inline editing, custom icons per element type), the builder becomes a significant piece of code. This is complexity in one place rather than spread across sync handlers, which is the correct tradeoff.

The One Thing to Watch For

When a drag-and-drop reorder allows cross-level moves (dragging an element into a group), add an explicit guard against circular DOM insertion. Moving a group into one of its own children calls parent.insertBefore(child, grandchild) where child contains parent, producing a detached subtree. Check srcElement.contains(targetElement) before any cross-level move.

Read this post in the full Engineering Journal โ†’