Engineering Journal
Schema Editor
Schema Editor

Postmortem: We Put Selection Handles Inside the SVG and Paid for It at Every Zoom Level

2026-06-04

TLDR

Selection handles rendered as SVG elements inside the diagram scaled with zoom (16px handles at 2x zoom, 4px at 0.5x), got clipped at the viewBox edge, and had hit zones that did not match their visual positions. Three separate bugs, one architectural mistake: ephemeral UI inside the content layer.


The Assumption That Seemed Reasonable

The diagram is SVG. Selection handles are part of the diagram interaction. SVG has good support for drawing rectangles and circles. The natural place to put handles is inside the SVG, alongside the diagram elements.

This seemed not just reasonable but obvious. The SVG coordinate system would position handles correctly relative to selected elements. SVG event handling would fire on handle clicks. No additional canvas element, no coordinate mapping, no second rendering layer.

When It Failed

The first failure was at zoom. At 2.0x zoom, the 6-unit SVG handle (6x6 SVG user units) rendered as 12 screen pixels. At 0.5x zoom, it rendered as 3 screen pixels, too small to click reliably. The handle size should be constant in screen pixels (a 6px target is always a 6px target), but inside the SVG it was constant in SVG units, which scaled with zoom.

The compensation was to divide the handle size by the current zoom level: size / zoom SVG units would render as size screen pixels. This worked but required passing the current zoom to every handle rendering call and updating handles on every zoom event.

The second failure was viewBox clipping. An element placed at the right edge of the viewport had its right-side handles clipped by the SVG viewBox boundary. The handles were visually cut off. Clicking the clipped half did nothing. The fix was to extend the SVG's viewBox slightly beyond the container, which introduced other problems (elements could be placed outside the visible area).

The third failure was hit zones. After the CSS-transform-to-viewBox migration, the handles' screen positions were recalculated using getBoundingClientRect() on the SVG child elements. This gave the wrong positions because getBoundingClientRect() on SVG children at that time did not account for the viewBox transform correctly in all browsers. Clicks on handles missed by the same amount the diagram was panned.

Three separate fixes. Each made the code more complex. Each was correct for a specific configuration and broke when another configuration changed.

What Was Actually Wrong

Handles are not diagram content. They are UI chrome that lives above the diagram. Putting them in the diagram layer meant they inherited all of the diagram layer's properties: scaling with zoom, clipping at the viewBox boundary, participating in SVG coordinate math.

The diagram layer is the wrong place for anything that needs to be pixel-constant, unclipped, and hit-tested independently of zoom.

What Got Deleted

All SVG-element handles: the <rect class="selection-handle"> elements added to the SVG on selection, the zoom-division compensation code, the viewBox-extension workaround, and the getBoundingClientRect-based hit test code.

Also deleted: a separate "handle layer" group element inside the SVG that tried to isolate handles from diagram content. The isolation helped with cleanup but did not solve the coordinate or clipping problems.

What Replaced It

A <canvas> overlay positioned above the SVG container via CSS (position: absolute). Handles are drawn with ctx.fillRect() at screen coordinates derived from getScreenCTM(). The handle size in the fillRect call is always 8 screen pixels, regardless of zoom. The canvas is sized by ResizeObserver, never clipped by anything. Hit testing reads from an explicit hit zone list updated on every frame.

The canvas overlay is three separate functions (init, render, hit-test) totaling about 80 lines. It replaced about 150 lines of SVG handle code plus three workarounds.

The Lesson

When you find yourself compensating for a layer's properties (dividing by zoom to counteract scaling, extending a boundary to counteract clipping), the thing you are drawing belongs in a different layer. The compensation is the symptom; the wrong layer is the cause.

Read this post in the full Engineering Journal →