@trimlinea/vue-panzoom
v0.1.0
Published
Performant, zero-dependency pan and zoom library for Vue 3 — composable-first, touch and pointer support.
Maintainers
Readme
vue-panzoom
Performant pan and zoom library for Vue 3 — composable-first, zero dependencies, touch and pointer support.
- Tiny — ~4.7 kb gzipped
- Zero dependencies (peer: Vue 3.3+)
- Composable-first —
usePanzoom()gives you reactive state and full control - Component wrapper —
<PanzoomContainer>for quick drop-in usage with scoped slots - Feature-rich — pan, zoom, pinch, wheel zoom, focal-point zoom, containment, SVG support
- TypeScript — fully typed API
Live Demo
Install
npm install @trimlinea/vue-panzoom
# or
pnpm add @trimlinea/vue-panzoom
# or
yarn add @trimlinea/vue-panzoom
# or
bun add @trimlinea/vue-panzoomQuick start — composable
<template>
<div class="parent">
<div ref="elementRef">
<img src="/photo.jpg" alt="Zoomable" />
</div>
</div>
<div class="controls">
<button @click="zoomIn()">+</button>
<button @click="zoomOut()">−</button>
<button @click="reset()">Reset</button>
<span>{{ scale.toFixed(2) }}×</span>
</div>
</template>
<script setup>
import { usePanzoom } from '@trimlinea/vue-panzoom'
const { elementRef, x, y, scale, zoomIn, zoomOut, reset } = usePanzoom({
maxScale: 5,
minScale: 0.5,
})
</script>Important: bind
ref="elementRef"on the element you want to pan/zoom, not its parent. The parent is automatically styled (overflow, touch-action, etc.).
Quick start — component
<template>
<div class="parent">
<PanzoomContainer
:options="{ maxScale: 5 }"
@zoom="(detail) => console.log('zoomed to', detail.scale)"
>
<template #default="{ scale, zoomIn, zoomOut, reset }">
<img src="/photo.jpg" alt="Zoomable" />
<div class="overlay">
<button @click="zoomIn()">+</button>
<button @click="zoomOut()">−</button>
<button @click="reset()">Reset</button>
<span>{{ scale.toFixed(2) }}×</span>
</div>
</template>
</PanzoomContainer>
</div>
</template>
<script setup>
import { PanzoomContainer } from '@trimlinea/vue-panzoom'
</script>Wheel zoom
When using the composable, bind the wheel handler on the parent:
<template>
<div class="parent" @wheel="zoomWithWheel">
<div ref="elementRef">…</div>
</div>
</template>The <PanzoomContainer> component handles this automatically (disable with :disable-wheel="true").
API reference
usePanzoom(options?)
Returns a reactive object with the following properties:
| Property | Type | Description |
|---|---|---|
| elementRef | Ref<HTMLElement \| SVGElement \| null> | Template ref — bind to the pan/zoom target |
| x | Readonly<Ref<number>> | Current X translation |
| y | Readonly<Ref<number>> | Current Y translation |
| scale | Readonly<Ref<number>> | Current scale |
| isPanning | Readonly<Ref<boolean>> | Whether the user is actively panning |
Methods
| Method | Signature | Description |
|---|---|---|
| pan | (x, y, opts?) → CurrentValues | Pan to absolute (or relative) coordinates |
| zoom | (scale, opts?) → CurrentValues | Zoom to an absolute scale |
| zoomIn | (opts?) → CurrentValues | Zoom in by one step (animated) |
| zoomOut | (opts?) → CurrentValues | Zoom out by one step (animated) |
| zoomToPoint | (scale, { clientX, clientY }, opts?) → CurrentValues | Zoom towards a screen point |
| zoomWithWheel | (WheelEvent, opts?) → CurrentValues | Handle a wheel event with focal-point zoom |
| handleKeydown | (KeyboardEvent) → void | Handle keyboard zoom/reset (+/=/-/0) |
| reset | (opts?) → CurrentValues | Reset to startX / startY / startScale |
| setOptions | (opts) → void | Merge new options at runtime |
| getOptions | () → PanzoomFullOptions | Get a copy of the current options |
| bind | () → void | Manually bind pointer listeners (when noBind: true) |
| destroy | () → void | Remove all listeners and reset styles |
| handleDown | (PointerEvent) → void | Low-level pointer-down handler |
| handleMove | (PointerEvent) → void | Low-level pointer-move handler |
| handleUp | (PointerEvent) → void | Low-level pointer-up handler |
<PanzoomContainer>
| Prop | Type | Default | Description |
|---|---|---|---|
| options | PanzoomFullOptions | {} | All panzoom options (reactive) |
| tag | string | 'div' | HTML tag for the rendered element |
| disable-wheel | boolean | false | Disable built-in wheel handler |
Events: @start, @change, @pan, @zoom, @reset, @end — each receives a PanzoomEventDetail payload.
Scoped slot props: x, y, scale, isPanning, pan, zoom, zoomIn, zoomOut, reset.
Exposed via ref: all composable properties/methods.
Options
Pan options
| Option | Type | Default | Description |
|---|---|---|---|
| contain | 'inside' \| 'outside' | — | Constrain element within or around its parent |
| cursor | string | 'move' | CSS cursor during interaction |
| disablePan | boolean | false | Disable panning |
| disableXAxis | boolean | false | Lock the X axis |
| disableYAxis | boolean | false | Lock the Y axis |
| relative | boolean | false | Treat coordinates as relative |
| panOnlyWhenZoomed | boolean | false | Disable pan at starting scale |
| roundPixels | boolean | false | Round to whole pixels |
Zoom options
| Option | Type | Default | Description |
|---|---|---|---|
| disableZoom | boolean | false | Disable zooming |
| focal | { x, y } | — | Zoom towards a point |
| minScale | number | 0.125 | Minimum scale |
| maxScale | number | 4 | Maximum scale |
| step | number | 0.3 | Step for wheel/pinch/zoomIn/zoomOut |
Animation options
| Option | Type | Default | Description |
|---|---|---|---|
| animate | boolean | false | Animate transitions |
| duration | number | 200 | Transition duration (ms) |
| easing | string | 'ease-in-out' | CSS easing |
Interaction options
| Option | Type | Default | Description |
|---|---|---|---|
| canvas | boolean | false | Bind to parent (drag anywhere) |
| exclude | Element[] | [] | Elements excluded from interaction |
| excludeClass | string | 'panzoom-exclude' | Class that excludes elements |
| handleStartEvent | (Event) → void | preventDefault + stopPropagation | Override start-event handling |
| noBind | boolean | false | Skip auto-binding; call bind() manually |
| overflow | string | 'hidden' | Parent overflow style |
| pinchAndPan | boolean | false | Allow pan during pinch |
| touchAction | string | 'none' | CSS touch-action |
| origin | string | '50% 50%' / '0 0' | Transform origin |
Initial values
| Option | Type | Default |
|---|---|---|
| startX | number | 0 |
| startY | number | 0 |
| startScale | number | 1 |
Transform
| Option | Type | Default | Description |
|---|---|---|---|
| setTransform | false \| function | built-in | Custom transform setter, or false to skip (use reactive :style instead) |
Event handlers
| Option | Type | Description |
|---|---|---|
| onStart | (detail) → void | Gesture started (pointerdown) |
| onChange | (detail) → void | Any transform update (pan, zoom, or reset) |
| onPan | (detail) → void | Pan transform applied |
| onZoom | (detail) → void | Zoom transform applied |
| onReset | (detail) → void | Transform was reset |
| onEnd | (detail) → void | Gesture ended (pointerup) |
Each callback receives a PanzoomEventDetail: { x, y, scale, isSVG, originalEvent? }.
Per-call options
These can be passed to pan(), zoom(), etc.:
| Option | Type | Description |
|---|---|---|
| force | boolean | Override disable guards |
| silent | boolean | Suppress events |
| animate | boolean | Override animation for this call |
Recipes
Custom transform (e.g. rotation)
Set setTransform: false to disable built-in transform application and drive it reactively from the template:
<template>
<div class="parent">
<div ref="elementRef" :style="{ transform }">…</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { usePanzoom } from '@trimlinea/vue-panzoom'
const { elementRef, x, y, scale } = usePanzoom({
setTransform: false,
})
const transform = computed(() =>
`rotate(0.5turn) scale(${scale.value}) translate(${x.value}px, ${y.value}px)`
)
</script>Alternatively, pass a function for imperative control:
const { elementRef } = usePanzoom({
setTransform(elem, { scale, x, y }) {
elem.style.transform = `rotate(0.5turn) scale(${scale}) translate(${x}px, ${y}px)`
},
})Canvas mode (drag the background)
const { elementRef } = usePanzoom({ canvas: true })Contained inside parent
const { elementRef } = usePanzoom({ contain: 'inside' })Keyboard shortcuts
handleKeydown handles +/= (zoom in), - (zoom out), and 0 (reset). Add tabindex to make the element focusable:
<template>
<div class="parent">
<div ref="elementRef" @keydown="handleKeydown" tabindex="0">…</div>
</div>
</template>
<script setup>
import { usePanzoom } from '@trimlinea/vue-panzoom'
const { elementRef, handleKeydown } = usePanzoom()
</script>SVG elements
<template>
<svg>
<g ref="elementRef">
<rect width="100" height="100" fill="blue" />
</g>
</svg>
</template>
<script setup>
import { usePanzoom } from '@trimlinea/vue-panzoom'
const { elementRef } = usePanzoom()
</script>Events
Pass callbacks directly in the composable options:
const { elementRef } = usePanzoom({
onStart: (detail) => console.log('gesture started', detail),
onChange: (detail) => console.log(detail), // { x, y, scale, isSVG, originalEvent }
onEnd: (detail) => console.log('gesture ended', detail),
})With <PanzoomContainer>, use Vue events as usual:
<PanzoomContainer @change="onPanzoomChange" @zoom="onZoom" />License
MIT
