# How DrawDB Implements Pan and Zoom on the Canvas: A Deep Dive

> Discover how DrawDB implements pan and zoom on its canvas. Explore the React context-based transform store and custom hooks that enable real-time SVG canvas updates.

- Repository: [drawDB/drawdb](https://github.com/drawdb-io/drawdb)
- Tags: deep-dive
- Published: 2026-08-14

---

**DrawDB implements pan and zoom through a React context-based transform store that captures scale and translation values, with mouse and wheel event handlers in a custom hook to update the transform and re-render the SVG canvas in real-time.**

DrawDB's canvas pan and zoom functionality relies on a lightweight but precise state management pattern. The implementation in the `drawdb-io/drawdb` repository avoids heavy libraries by using React's built-in context system to synchronize transform state across the diagram surface. This article breaks down exactly how each piece works, with source references from the actual codebase.

## The TransformContext: Central State for Scale and Offset

All pan and zoom data lives in [`src/context/TransformContext.js`](https://github.com/drawdb-io/drawdb/blob/main/src/context/TransformContext.js). This file creates a plain React context that stores three mutable values:

- `scale` — the current zoom level
- `offsetX` — horizontal translation in pixels
- `offsetY` — vertical translation in pixels

The context also exposes `setTransform`, a function to batch-update these values.

The provider wraps the entire application in [`App.jsx`](https://github.com/drawdb-io/drawdb/blob/main/App.jsx), ensuring any component can subscribe to transform changes. This design means the diagram, toolbar, and minimap all share the same source of truth without prop drilling.

## useTransform: The Consumption Hook

To access the context, components import `useTransform` from [`src/hooks/useTransform.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useTransform.js):

```javascript
import { useContext } from "react";
import { TransformContext } from "../context/TransformContext";

export default function useTransform() {
  return useContext(TransformContext);
}

```

This thin wrapper standardizes how components read the current pan and zoom state. Any call to `useTransform()` returns the latest `scale`, `offsetX`, `offsetY`, and `setTransform` function.

## useCanvas: Event Handling and Transform Calculation

The heavy lifting happens in [`src/hooks/useCanvas.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useCanvas.js). This hook registers event listeners on the canvas element and computes new transform values based on user input.

### Panning Implementation

On `mousedown`, the hook records the initial cursor position. During `mousemove` with the button held, it calculates the delta between current and starting positions, then updates `offsetX` and `offsetY` by that delta. This creates the dragging sensation where the canvas follows the cursor.

### Zooming Implementation

The `wheel` event handler performs smooth zooming centered on the cursor position. The algorithm ensures the point under the mouse stays stationary while the scale changes—a critical detail for intuitive navigation.

Here is the core zoom logic from [`src/hooks/useCanvas.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useCanvas.js):

```javascript
function handleWheel(e) {
  e.preventDefault();
  const { scale, offsetX, offsetY } = transform;
  
  // Calculate zoom factor from wheel delta
  const zoom = Math.exp(-e.deltaY * 0.001);
  
  // Get cursor position relative to canvas
  const rect = e.currentTarget.getBoundingClientRect();
  const cx = e.clientX - rect.left;
  const cy = e.clientY - rect.top;
  
  const newScale = scale * zoom;
  
  // Adjust offsets so (cx, cy) remains fixed in screen space
  const newOffsetX = cx - (cx - offsetX) * (newScale / scale);
  const newOffsetY = cy - (cy - offsetY) * (newScale / scale);
  
  setTransform({
    scale: newScale,
    offsetX: newOffsetX,
    offsetY: newOffsetY
  });
}

```

The math here is worth unpacking:

1. `Math.exp(-e.deltaY * 0.001)` converts linear wheel deltas into exponential scale factors, giving consistent zoom speed at any magnification
2. The offset adjustment formulas translate the zoom origin from the top-left corner (default SVG behavior) to the cursor location

On component unmount, the hook removes all listeners to prevent memory leaks.

## Applying the Transform to the SVG Canvas

The diagram component—typically [`src/components/Diagram.jsx`](https://github.com/drawdb-io/drawdb/blob/main/src/components/Diagram.jsx)—reads the transform context and applies it directly to the SVG element:

```jsx
const { scale, offsetX, offsetY } = useTransform();

return (
  <svg
    style={{ cursor: isPanning ? "grabbing" : "grab" }}
    transform={`translate(${offsetX}, ${offsetY}) scale(${scale})`}
    {...canvasProps}
  >
    {/* diagram nodes, relationships, etc. */}
  </svg>
);

```

Because `useTransform` subscribes to context changes, every pan or zoom operation triggers a re-render with the updated transform string. SVG's `transform` attribute handles the matrix math natively, keeping the JavaScript overhead minimal.

The cursor style switches between `grab` and `grabbing` based on the internal `isPanning` state, providing immediate visual feedback during drag operations.

## Key Design Decisions

**Context over refs.** DrawDB could have used refs and direct DOM manipulation for slightly better performance. However, the context approach keeps the transform inspectable, time-travel debuggable, and accessible to React DevTools.

**Pure SVG transforms.** By using the SVG `transform` attribute rather than CSS transforms, DrawDB ensures precise coordinate mapping for hit-testing diagram elements at any zoom level.

**Fixed-point zooming.** The offset recalculation in `handleWheel` eliminates the "drift" problem where zooming repeatedly causes the viewport to wander away from the targeted area.

## Summary

- `TransformContext` in [`src/context/TransformContext.js`](https://github.com/drawdb-io/drawdb/blob/main/src/context/TransformContext.js) holds the single source of truth for `scale`, `offsetX`, and `offsetY`
- `useTransform` in [`src/hooks/useTransform.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useTransform.js) provides standardized access to the context
- `useCanvas` in [`src/hooks/useCanvas.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useCanvas.js) implements mouse-drag panning and wheel zooming with cursor-centered scaling
- The diagram component applies the transform via SVG's native `transform` attribute for hardware-accelerated rendering
- All state updates flow through React's context system, enabling instantaneous UI synchronization

## Frequently Asked Questions

### How does DrawDB keep the zoom centered on the cursor rather than the origin?

The `handleWheel` function in [`src/hooks/useCanvas.js`](https://github.com/drawdb-io/drawdb/blob/main/src/hooks/useCanvas.js) recalculates `offsetX` and `offsetY` using the formula `newOffset = cursor - (cursor - oldOffset) * (newScale / oldScale)`. This transformation moves the coordinate system origin so that the point under the cursor maintains the same screen position after scaling.

### What zoom sensitivity does DrawDB use for mouse wheels?

The implementation uses `Math.exp(-e.deltaY * 0.001)`, which translates wheel deltas into approximately 0.1% scale change per wheel tick. This exponential mapping keeps zooming feel consistent regardless of current magnification level.

### Could this pan and zoom implementation work with HTML Canvas instead of SVG?

Yes, with modifications. The state management in `TransformContext` and `useTransform` is rendering-agnostic. For HTML Canvas, you would replace the SVG `transform` attribute with `ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY)` in your render loop, keeping the same event handling logic from `useCanvas`.

### Why does DrawDB use a React context instead of a state management library like Redux?

The transform state is localized and updated frequently (every mouse move during panning). React context provides sufficient performance for this use case without adding external dependencies. The codebase prioritizes minimal bundle size and direct control over re-render boundaries.