Custom elements wrapper ZoomPinch - reactive pinch & zoom component
npm install @zoompinch/elementsWeb Components (Custom Elements) 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/elements
`html
translate-x="0"
translate-y="0"
scale="1"
rotate="0"
min-scale="0.5"
max-scale="4"
offset-top="0"
offset-right="0"
offset-bottom="0"
offset-left="0"
clamp-bounds="false"
rotation="true"
zoom-speed="1"
translate-speed="1"
zoom-speed-apple-trackpad="1"
translate-speed-apple-trackpad="1"
>
`
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| translate-x | number | 0 | X translation in pixels |translate-y
| | number | 0 | Y translation in pixels |scale
| | number | 1 | Current scale factor |rotate
| | number | 0 | Rotation in radians |min-scale
| | number | 0.1 | Minimum scale (user gestures only) |max-scale
| | number | 10 | Maximum scale (user gestures only) |offset-top
| | number | 100 | Top padding in pixels |offset-right
| | number | 0 | Right padding in pixels |offset-bottom
| | number | 0 | Bottom padding in pixels |offset-left
| | number | 0 | Left padding in pixels |clamp-bounds
| | "true" \| "false" | "false" | Clamp panning within bounds (user gestures only) |rotation
| | "true" \| "false" | "true" | Enable rotation gestures |
Note: min-scale, max-scale, rotation, and clamp-bounds only apply during user interaction. Programmatic changes via methods are unrestricted.
| Event | Description |
|-------|-------------|
| update | Fired when transform changes (attributes are updated) |init
| | Fired when the engine is ready |
`javascript`
zoomPinch.addEventListener('update', () => {
const translateX = zoomPinch.getAttribute('translate-x');
const translateY = zoomPinch.getAttribute('translate-y');
const scale = zoomPinch.getAttribute('scale');
const rotate = zoomPinch.getAttribute('rotate');
});
Access methods directly on the element:
`javascript
const zoomPinch = document.querySelector('zoom-pinch');
// Call methods
zoomPinch.applyTransform(scale, wrapperCoords, canvasCoords);
zoomPinch.normalizeClientCoords(clientX, clientY);
zoomPinch.composePoint(x, y);
// Access properties
zoomPinch.canvasWidth;
zoomPinch.canvasHeight;
`
#### applyTransform(scale, wrapperCoords, canvasCoords)
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)
Examples:
`javascript
// Center canvas at scale 1
zoomPinch.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
// Zoom to 2x, keep centered
zoomPinch.applyTransform(2, [0.5, 0.5], [0.5, 0.5]);
// Anchor canvas top-left to wrapper center
zoomPinch.applyTransform(1.5, [0.5, 0.5], [0, 0]);
`
#### 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:
`javascript`
zoomPinch.addEventListener('click', (e) => {
const [x, y] = zoomPinch.normalizeClientCoords(e.clientX, e.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:
`javascript`
// Get wrapper position for canvas center
const [wrapperX, wrapperY] = zoomPinch.composePoint(
zoomPinch.canvasWidth / 2,
zoomPinch.canvasHeight / 2
);
Access current canvas dimensions:
`javascript`
const width = zoomPinch.canvasWidth; // number
const height = zoomPinch.canvasHeight; // number
Use slot="matrix" for overlay elements that follow the canvas transform.
Note: Matrix elements must be updated manually on the update event.
Example:
`html

`
Absolute pixels within canvas content.
- Origin: (0, 0) at top-left0
- Range: to canvasWidth, 0 to canvasHeight
`javascript`
const [canvasX, canvasY] = zoomPinch.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
`javascript`
const [wrapperX, wrapperY] = zoomPinch.composePoint(canvasX, canvasY);
Normalized coordinates for applyTransform.0.0
- Range: to 1.00.5
- = center, 1.0 = bottom-right
`javascript`
[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:
`html`

2. Center content on init:
`javascript`
zoomPinch.addEventListener('init', () => {
zoomPinch.applyTransform(1, [0.5, 0.5], [0.5, 0.5]);
});
3. Prevent image drag:
`html`

4. Update matrix overlays on transform change:
`javascript`
zoomPinch.addEventListener('update', updateMatrix);
The element uses Shadow DOM. Style the host:
`css`
zoom-pinch {
display: block;
width: 800px;
height: 600px;
border: 1px solid #ccc;
}
Internal structure (Shadow DOM):
`css``
:host / Container /
.content / Wrapper /
.canvas / Canvas wrapper /
.matrix / Matrix overlay /
- ✅ Chrome/Edge (latest)
- ✅ Firefox (latest)
- ✅ Safari (latest, including iOS)
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
MIT
- @zoompinch/core - Core engine
- @zoompinch/vue - Vue 3
- @zoompinch/react - React
Built with ❤️ by Elya Maurice Conrad