Vue wrapper for ZoomPinch - reactive pinch & zoom component
npm install @zoompinch/vueVue 3 component for @zoompinch/core - Apply a pinch-and-zoom experience that’s feels native and communicates the transform reactively and lets you project any layer on top of the transformed canvas.
Play with the demo: https://zoompinch.pages.dev
Unlike other libraries, _Zoompinch_ does not just uses the center point between two fingers as projection center. The fingers get correctly projected on the virtual canvas. This makes pinching on touch devices feel native-like.
Adside of touch, mouse and wheel events, gesture events (Safari Desktop) are supported as well! Try it out on the demo
``bash`
npm install @zoompinch/vue
`vue
v-model:transform="transform"
:offset="{ top: 0, right: 0, bottom: 0, left: 0 }"
:min-scale="0.5"
:max-scale="4"
:clamp-bounds="false"
:rotation="true"
:zoom-speed="1"
:translate-speed="1"
:zoom-speed-apple-trackpad="1"
:translate-speed-apple-trackpad="1"
:mouse="false"
:wheel="true"
:touch="true"
:gesture="true"
@init="handleInit"
@click="handleClick"
>
`
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| transform | Transform | { translateX: 0, translateY: 0, scale: 1, rotate: 0 } | Current transform state (v-model) |offset
| | Offset | { top: 0, right: 0, bottom: 0, left: 0 } | Inner padding/offset within container |min-scale
| | number | 0.5 | Minimum scale (user gestures only) |max-scale
| | number | 10 | Maximum scale (user gestures only) |clamp-bounds
| | boolean | false | Clamp panning within bounds (user gestures only) |rotation
| | boolean | true | Enable rotation gestures |mouse
| | boolean | true | Enable mouse drag |wheel
| | boolean | true | Enable wheel/trackpad |touch
| | boolean | true | Enable touch gestures |gesture
| | boolean | true | Enable Safari gesture events |
#### The Problem
Pan and zoom interactions behave differently across input devices:
- Apple Trackpads: Provide smooth, precise scroll values with natural momentum
- Mouse Wheels: Send large, discrete jumps (typically ±100 or ±120 per scroll tick)
Without normalization, this causes:
- Uncomfortably large zoom jumps when using mouse wheels
- Panning that's either too slow (trackpad-optimized) or too fast (mouse-optimized)
- Inconsistent user experience across Windows, Mac, and Linux
#### The Solution
The library automatically detects the input device type and applies different speed multipliers:
- Trackpad gestures use base values for smooth, 1:1 response
- Mouse wheel actions use amplified values for comfortable discrete steps
You can fine-tune these multipliers for your specific use case using the speed props.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| translate-speed | number | 1 | Pan speed multiplier for mouse wheels |zoom-speed
| | number | 1 | Zoom speed multiplier for mouse wheels |translate-speed-apple-trackpad
| | number | 1 | Pan speed multiplier for trackpads |zoom-speed-apple-trackpad
| | number | 1 | Zoom speed multiplier for trackpads |
Note: min-scale, max-scale, rotation, and clamp-bounds only apply during user interaction. Programmatic changes via ref methods are unrestricted.
| Event | Payload | Description |
|-------|---------|-------------|
| @init | void | Fired when canvas dimensions are available |@update:transform
| | Transform | Fired when transform changes (v-model) |
`vue`
@update:transform="handleTransformUpdate"
>
Access methods via template ref:
`typescript
const zoompinchRef = ref
// Call methods
zoompinchRef.value?.applyTransform(scale, wrapperCoords, canvasCoords, rotate?);
zoompinchRef.value?.normalizeClientCoords(clientX, clientY);
zoompinchRef.value?.composePoint(x, y);
zoompinchRef.value?.rotateCanvas(x, y, radians);
// Access properties
zoompinchRef.value?.canvasWidth;
zoompinchRef.value?.canvasHeight;
`
#### applyTransform(scale, wrapperCoords, canvasCoords, rotate?)
Apply transform by anchoring a canvas point to a wrapper point.
Parameters:
- scale: number - Target scalewrapperCoords: [number, number]
- - Wrapper position (0-1, 0.5 = center)canvasCoords: [number, number]
- - Canvas position (0-1, 0.5 = center)rotate?: number
- - Optional rotation in radians
Examples:
`typescript
// Center canvas at scale 1
zoompinchRef.value?.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
// Zoom to 2x, keep centered
zoompinchRef.value?.applyTransform(2, [0.5, 0.5], [0.5, 0.5]);
// Anchor canvas top-left to wrapper center
zoompinchRef.value?.applyTransform(1.5, [0.5, 0.5], [0, 0]);
// Set rotation
zoompinchRef.value?.applyTransform(1, [0.5, 0.5], [0.5, 0.5], Math.PI / 4);
`
#### normalizeClientCoords(clientX, clientY)
Convert global client coordinates to canvas coordinates.
Parameters:
- clientX: number - Global X from eventclientY: number
- - Global Y from event
Returns: [number, number] - Canvas coordinates in pixels
Example:
`typescript`
function handleClick(event: MouseEvent) {
const [x, y] = zoompinchRef.value!.normalizeClientCoords(
event.clientX,
event.clientY
);
console.log('Canvas position:', x, y);
}
#### composePoint(x, y)
Convert canvas coordinates to wrapper coordinates (accounts for transform).
Parameters:
- x: number - Canvas X in pixelsy: number
- - Canvas Y in pixels
Returns: [number, number] - Wrapper coordinates in pixels
Example:
`typescript`
// Get wrapper position for canvas center
const [wrapperX, wrapperY] = zoompinchRef.value!.composePoint(
canvasWidth / 2,
canvasHeight / 2
);
#### rotateCanvas(x, y, radians)
Rotate canvas around a specific canvas point.
Parameters:
- x: number - Canvas X (rotation center)y: number
- - Canvas Y (rotation center)radians: number
- - Rotation angle
Example:
`typescript`
// Rotate 90° around canvas center
const centerX = zoompinchRef.value!.canvasWidth / 2;
const centerY = zoompinchRef.value!.canvasHeight / 2;
zoompinchRef.value?.rotateCanvas(centerX, centerY, Math.PI / 2);
Access current canvas dimensions:
`typescript`
const width = zoompinchRef.value?.canvasWidth; // number
const height = zoompinchRef.value?.canvasHeight; // number
Scoped slot for rendering overlay elements that follow the canvas transform.
Scoped Props:
| Prop | Type | Description |
|------|------|-------------|
| composePoint | (x: number, y: number) => [number, number] | Canvas → Wrapper coords |normalizeClientCoords
| | (clientX: number, clientY: number) => [number, number] | Client → Canvas coords |canvasWidth
| | number | Current canvas width |canvasHeight
| | number | Current canvas height |
Note: applyTransform and rotateCanvas are NOT available in the slot. Use component ref instead.
Example:
`vue`

Absolute pixels within canvas content.
- Origin: (0, 0) at top-left0
- Range: to canvasWidth, 0 to canvasHeight
`typescript`
const [canvasX, canvasY] = normalizeClientCoords(event.clientX, event.clientY);
Absolute pixels within viewport/wrapper.
- Origin: (0, 0) at top-left (accounting for offset)0
- Range: to wrapperWidth, 0 to wrapperHeight
`typescript`
const [wrapperX, wrapperY] = composePoint(canvasX, canvasY);
Normalized coordinates for applyTransform.0.0
- Range: to 1.00.5
- = center, 1.0 = bottom-right
`typescript`
[0, 0] // top-left
[0.5, 0.5] // center
[1, 1] // bottom-right
Conversion Flow:
``
Client Coords → normalizeClientCoords() → Canvas Coords → composePoint() → Wrapper Coords
1. Always specify image dimensions to avoid layout shifts:
`vue`

2. Center content on init:
`typescript`
function handleInit() {
zoompinchRef.value?.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
}
3. Prevent image drag:
`vue`

4. Use clamp bounds:
`vue`
Styling
Minimal base styles are applied. Customize via class or style:
`vue`
style="width: 100%; height: 600px; border: 1px solid #ccc;"
>
Internal CSS classes:
`css``
.zoompinch / Container /
.zoompinch > .canvas / Canvas wrapper /
.zoompinch > .matrix / Matrix overlay /
- ✅ Chrome/Edge (latest)
- ✅ Firefox (latest)
- ✅ Safari (latest, including iOS)
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
MIT
---
- @zoompinch/core - Core engine
- @zoompinch/react - React
- @zoompinch/elements - Web Components
---
Built with ❤️ by Elya Maurice Conrad