SVG Pan Zoom Matrix Transformations Explained

Libraries like svg-pan-zoom manage interactive panning and zooming by intercepting pointer events and applying affine matrix transformations to an SVG container. During viewport dragging, the library calculates the displacement between mouse or touch positions, converts screen-space deltas into the SVG coordinate space, updates the 2D transformation matrix, and writes the resulting matrix back to the target SVG element.

The 2D Affine Transformation Matrix

SVG elements use a 3x3 affine transformation matrix represented in CSS and SVG attributes as a 6-value tuple: matrix(a, b, c, d, e, f).

When a user drags the viewport, the scale (a, d) and skew (b, c) components remain constant, while the translation components (e, f) are continuously updated to shift the rendered graphics across the screen.

Capturing Pointer Events and Calculating Deltas

The panning cycle begins when the user triggers a drag action:

  1. Pointer Down (mousedown / pointerdown): The library records the initial coordinates \((x_0, y_0)\) in screen space (e.g., event.clientX and event.clientY) and sets an active dragging state. It also retrieves the current transformation matrix of the viewport.
  2. Pointer Move (mousemove / pointermove): As the pointer moves to a new position \((x_t, y_t)\), the library computes the displacement vector: \[\Delta x = x_t - x_{prev}\] \[\Delta y = y_t - y_{prev}\] The previous position is then updated to \((x_t, y_t)\) for the next frame.
  3. Pointer Up (mouseup / pointerup): The drag state is cleared, and event listeners are reset or detached.

Coordinate Space Conversion

Screen pixels do not always have a 1:1 relationship with SVG user units due to CSS scaling, device pixel ratios, and existing zoom levels. To prevent erratic movement during drags:

  1. The library accesses the element’s Current Transformation Matrix using svgElement.getScreenCTM().
  2. To translate screen coordinates to SVG coordinates, the library calculates the inverse matrix via getScreenCTM().inverse().
  3. The displacement \((\Delta x, \Delta y)\) is mapped through this inverse transformation. When panning under uniform scaling, this simplifies to dividing the screen-space displacement by the current zoom level: \[\Delta x_{svg} = \frac{\Delta x}{scale}\] \[\Delta y_{svg} = \frac{\Delta y}{scale}\]

Updating and Applying the Matrix

Once the correct translation delta is determined, the library updates the transformation matrix:

  1. Matrix Translation: A translation matrix representing the movement is premultiplied or added to the existing transformation matrix: \[e_{new} = e_{current} + \Delta x\] \[f_{new} = f_{current} + \Delta y\]
  2. DOM Update: The new matrix values are written to the target viewport layer—typically a top-level <g class="svg-pan-zoom_viewport"> element grouping all visible SVG elements.

The update is applied either via the transform attribute:

<g transform="matrix(a, b, c, d, e_new, f_new)">
  <!-- SVG child nodes -->
</g>

or via the SVG DOM interface:

const matrix = viewportElement.getCTM();
matrix.e = newE;
matrix.f = newF;

const transform = svgRoot.createSVGTransformFromMatrix(matrix);
viewportElement.transform.baseVal.initialize(transform);

By wrapping all visual nodes in a single transformed group and mutating its matrix via hardware-accelerated transforms, libraries achieve smooth, high-performance viewport dragging without needing to recalculate the individual coordinates of underlying vector shapes.