Engineering Journal
Schema Editor
Schema Editor

Postmortem: The Layer Panel Had Its Own Data Model and It Was Always Wrong

2026-06-04

TLDR

The layer panel maintained its own folder tree separate from the SVG DOM. Canvas operations that added or moved elements without notifying the panel left the panel showing stale data. The panel was visually consistent for operations done through the panel and wrong for everything else.


The Assumption That Seemed Reasonable

A layer panel needs rich UI: drag-and-drop reordering, expand/collapse groups, rename inline, visibility toggle, lock toggle. Building this on top of a live DOM walk seemed expensive and fragile. The natural design was a panel data model: an array of layer objects, each with a name, id, visibility flag, and children array for groups.

The canvas and the panel model would stay in sync through a simple protocol: canvas emits events, panel listens and updates its model, panel operations call canvas methods and then update the model.

This design is how Figma's layers panel conceptually works. It is how VS Code's file explorer works. It seemed correct.

When It Failed

The first desync appeared two weeks into development. Undo was implemented by restoring a full canvas snapshot. The snapshot replaced the SVG DOM contents. The panel model was not part of the snapshot. After undo, the canvas showed the restored state and the panel showed the pre-undo state.

A panel rebuild was added to the undo handler. This fixed the symptom but exposed the pattern: every operation that replaced DOM content needed to trigger a panel rebuild.

The second desync appeared with group operations. Grouping selected elements wrapped them in a <g> element and updated the DOM. The panel model was updated with a new group entry. But the group's children in the model were references to the old flat entries, not to the new DOM children. Ungrouping later left orphaned entries in the model.

The third desync appeared with import. SVG import added elements directly to the DOM without going through the canvas event system. The panel model was not updated. Imported elements appeared on the canvas and were invisible in the panel.

The fourth desync was the most subtle: z-order. The panel model stored elements in insertion order. The DOM was reordered by operations like "bring to front" and "send to back." The model and DOM had different orders. Users would drag a panel entry to reorder and the result would snap to a different position because the model's index did not match the DOM's index.

What Was Actually Wrong

The panel model was a denormalized copy of the DOM state. It was correct at creation and drifted on every subsequent change. The sync protocol had to cover every possible mutation path. The mutation paths multiplied faster than the sync handlers.

The root problem: the panel was trying to be a data source when it should have been a view.

What Got Deleted

The entire panel data model: the layers array, the groups map, the LayerEntry class, and all event listeners that tried to keep them in sync with the DOM. Also deleted: the _buildUserGroupSection function that built a separate folder section for user-created groups, disconnected from the main element list.

Combined, this was about 200 lines of sync logic that was partially correct on a good day.

What Replaced It

A buildPanel function that walks the SVG DOM directly. It is called after every canvas operation that might change the element set. The panel reads all state from the DOM: element names from data-layer-name attributes, visibility from data-hidden attributes, lock state from data-locked attributes, z-order from DOM position.

There is no panel model to get out of sync. There is no sync protocol. There is one function that produces the correct panel state from the current DOM.

The Lesson

A panel that is a view of a data store is only as correct as the synchronization between the panel and the store. If the store is the render tree (the DOM), every synchronization layer adds a way to be wrong. Remove the layer: walk the tree, build the view, repeat on every change.

Read this post in the full Engineering Journal →