Layer Panels That Don't Mirror the DOM Are Theater
TLDR
Maintaining a secondary data model for layer panels in SVG web apps is architectural anti-pattern. While dual-model architectures (separate layer arrays) seem clean in desktop tools like Figma or Photoshop, web applications built on SVG DOM do not own an underlying rendering engine. The SVG DOM is the rendering engine. Deriving the layer panel UI directly from DOM nodes guarantees 100% synchronization and costs under 1ms per rebuild for typical diagrams.| Architectural Choice | State Source | Desynchronization Points | Rebuild Overhead |
|---|---|---|---|
| Secondary Data Model | Parallel JS object arrays | Undo/redo, import, paste, script edits | Fragile state sync logic |
| DOM-Derived Pure View | SVG DOM Node Tree | None (Single Source of Truth) | < 1ms for typical diagrams |
Problem statement: the myth of desktop dual-model parity
Desktop graphics engines (Figma, After Effects, Photoshop) maintain separate layer models because their layer tree is their internal scene graph.
When web developers copy this pattern in SVG canvas applications, they create a secondary data model that attempts to shadow the browser's DOM tree.
Because web applications expose dozens of ways to mutate the DOM (direct mouse drag, keyboard shortcuts, paste buffers, third-party SVG imports, programmatic API calls), keeping a secondary JS layer model in sync with the DOM requires intercepting every possible DOM mutation path.
Technical failure mode: Undo/Redo & import breakdown
- Undo/Redo Desync: Restoring a canvas state via DOM snapshot replacement mutates the entire DOM tree in a single operation. Secondary layer models fail to receive individual event notifications, leaving the panel displaying pre-undo state.
- Import Bypass: Importing an external SVG appends new elements directly to the DOM tree. Secondary layer models remain unaware of the imported elements, causing them to render on canvas while remaining invisible in the layer panel.
The fix: pure view DOM projections
Rebuild the layer panel directly from DOM node order after canvas operations:
// Pure View Projection: DOM IS THE MODEL
function afterCanvasOperation() {
applyCanvasMutation();
// Rebuild layer UI from DOM nodes (Fast: <1ms for 500 nodes)
rebuildLayerPanelFromDom();
}
To preserve UI state (such as collapsed group folders or scroll positions) across rebuild passes, capture active UI keys before clearing the panel container and restore them after:
function rebuildLayerPanelFromDom() {
const collapsedIds = getCollapsedFolderSet();
const scrollTop = panelContainer.scrollTop;
panelContainer.innerHTML = ''; Array.from(contentRoot.children).forEach(el => { const row = renderRowForElement(el, collapsedIds); panelContainer.appendChild(row); });
panelContainer.scrollTop = scrollTop; }
Rule of thumb: Treat your layer panel as a pure view projection of the DOM. Never write secondary layer state synchronization layers when walking the DOM tree yields instant, guaranteed parity.