Pan Zoom
Headless pan and zoom container with pointer, pinch, wheel, and keyboard input.
|
|
| Package |
@vizejs/ui/pan-zoom |
| Maturity |
stable |
| Own the source |
vize lib pull pan-zoom |
| Requires |
context, controllable-state |
| Aliases |
pan zoom, zoomable, zoom pan pinch, image zoom, canvas viewport |
| Covers |
react-zoom-pan-pinch, panzoom, Pointer Events pinch gestures, WheelEvent deltaMode |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { PanZoomContent, PanZoomFit, PanZoomReset, PanZoom, PanZoomRoot, PanZoomStatus, PanZoomViewport, PanZoomZoomIn, PanZoomZoomOut } from "@vizejs/ui/pan-zoom";
Or copy the source into your project with vize lib pull pan-zoom (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Floor plan viewer with pointer, wheel, and keyboard pan and zoom plus zoom, reset, and fit buttons. -->
<script setup lang="ts">
import {
PanZoom,
PanZoomContent,
PanZoomFit,
PanZoomReset,
PanZoomStatus,
PanZoomViewport,
PanZoomZoomIn,
PanZoomZoomOut,
} from "@vizejs/ui/pan-zoom";
// The viewport needs a size to clip and fit its content.
const viewportSize = { inlineSize: "480px", blockSize: "320px" };
</script>
<template>
<PanZoom :max-scale="4" bounds="contain">
<PanZoomViewport aria-label="Office floor plan" :style="viewportSize">
<PanZoomContent>
<img
src="https://example.com/media/floor-plan.png"
alt="Office floor plan"
width="960"
height="640"
/>
</PanZoomContent>
</PanZoomViewport>
<PanZoomZoomIn />
<PanZoomZoomOut />
<PanZoomReset />
<PanZoomFit />
<PanZoomStatus />
</PanZoom>
</template>
API
PanZoomContent
Source: src/families/media/pan-zoom/pan-zoom-content.vue
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Transformed content. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered content element. |
PanZoomFit
Source: src/families/media/pan-zoom/pan-zoom-fit.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name for icon-only content. Without slot content the localized messages.fit label is rendered as text. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the action. Call preventDefault() to keep the transform. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Button content. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
disabled |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
PanZoomReset
Source: src/families/media/pan-zoom/pan-zoom-reset.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name for icon-only content. Without slot content the localized messages.reset label is rendered as text. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the action. Call preventDefault() to keep the transform. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Button content. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
disabled |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
PanZoom
Source: src/families/media/pan-zoom/pan-zoom-root.vue
Props
| Prop |
Type |
Default |
Description |
modelValue |
PanZoomTransform |
undefined |
Controlled transform (v-model). undefined selects uncontrolled behavior. |
defaultValue |
PanZoomTransform |
{ x: 0, y: 0, scale: 1 } |
Initial uncontrolled transform, also restored by reset() and the 0 key. |
minScale |
number |
0.25 |
Smallest zoom factor. |
maxScale |
number |
8 |
Largest zoom factor. |
bounds |
PanZoomBounds |
"none" |
Pan and zoom constraint applied to every change. |
zoomStep |
number |
1.25 |
Multiplicative factor of one zoom-in or zoom-out step. |
panStep |
number |
40 |
Viewport pixels moved by one arrow key press (Shift moves four times as far). |
wheelMode |
PanZoomWheelMode |
"zoom" |
Wheel behavior. Trackpad pinches (ctrlKey wheels) always zoom. |
doubleClickZoom |
boolean |
true |
Zoom in one step around the pointer on double-click (Shift zooms out). |
fitPadding |
number |
0 |
Inset kept around the content by fit() and the Home key. |
contentSize |
PanZoomSize |
undefined |
Unscaled content size. undefined measures the PanZoomContent element. |
disabled |
boolean |
false |
Suppress every pointer, wheel, keyboard, and button interaction. |
messages |
PanZoomMessages |
undefined |
Localized role description, button labels, and zoom announcement. |
Events
| Event |
Payload |
Description |
update:modelValue |
[transform: PanZoomTransform] |
Fired with every requested transform. |
change |
[transform: PanZoomTransform, previous: PanZoomTransform, source: PanZoomChangeSource] |
Fired after every distinct transform request with its source. |
transformStart |
[source: PanZoomChangeSource] |
Fired when a continuous gesture (pan, pinch, wheel burst) starts. |
transformEnd |
[source: PanZoomChangeSource] |
Fired when a continuous gesture ends. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Viewport, content, controls, and status. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
canZoomIn |
— |
|
canZoomOut |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
fit |
— |
|
panBy |
— |
|
reset |
— |
|
scale |
— |
|
setTransform |
— |
|
state |
— |
|
transform |
— |
|
zoomIn |
— |
|
zoomOut |
— |
|
zoomTo |
— |
|
PanZoomRoot
Source: src/families/media/pan-zoom/pan-zoom-root.vue
Props
| Prop |
Type |
Default |
Description |
modelValue |
PanZoomTransform |
undefined |
Controlled transform (v-model). undefined selects uncontrolled behavior. |
defaultValue |
PanZoomTransform |
{ x: 0, y: 0, scale: 1 } |
Initial uncontrolled transform, also restored by reset() and the 0 key. |
minScale |
number |
0.25 |
Smallest zoom factor. |
maxScale |
number |
8 |
Largest zoom factor. |
bounds |
PanZoomBounds |
"none" |
Pan and zoom constraint applied to every change. |
zoomStep |
number |
1.25 |
Multiplicative factor of one zoom-in or zoom-out step. |
panStep |
number |
40 |
Viewport pixels moved by one arrow key press (Shift moves four times as far). |
wheelMode |
PanZoomWheelMode |
"zoom" |
Wheel behavior. Trackpad pinches (ctrlKey wheels) always zoom. |
doubleClickZoom |
boolean |
true |
Zoom in one step around the pointer on double-click (Shift zooms out). |
fitPadding |
number |
0 |
Inset kept around the content by fit() and the Home key. |
contentSize |
PanZoomSize |
undefined |
Unscaled content size. undefined measures the PanZoomContent element. |
disabled |
boolean |
false |
Suppress every pointer, wheel, keyboard, and button interaction. |
messages |
PanZoomMessages |
undefined |
Localized role description, button labels, and zoom announcement. |
Events
| Event |
Payload |
Description |
update:modelValue |
[transform: PanZoomTransform] |
Fired with every requested transform. |
change |
[transform: PanZoomTransform, previous: PanZoomTransform, source: PanZoomChangeSource] |
Fired after every distinct transform request with its source. |
transformStart |
[source: PanZoomChangeSource] |
Fired when a continuous gesture (pan, pinch, wheel burst) starts. |
transformEnd |
[source: PanZoomChangeSource] |
Fired when a continuous gesture ends. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Viewport, content, controls, and status. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
transform |
PanZoomTransform |
Current transform. |
scale |
number |
Current zoom factor. |
state |
PanZoomState |
Current interaction state. |
canZoomIn |
boolean |
Whether zooming in is possible. |
canZoomOut |
boolean |
Whether zooming out is possible. |
element |
HTMLDivElement | null |
Rendered root element. |
setTransform |
(transform: PanZoomTransform) => boolean |
Request a transform (constrained by scale limits and bounds). Reports a change. |
zoomTo |
(scale: number, point?: PanZoomPoint) => boolean |
Zoom to scale around a viewport point (default: the viewport center). |
zoomIn |
(point?: PanZoomPoint) => boolean |
Zoom in by one zoomStep around a viewport point (default: center). |
zoomOut |
(point?: PanZoomPoint) => boolean |
Zoom out by one zoomStep around a viewport point (default: center). |
panBy |
(dx: number, dy: number) => boolean |
Translate by a viewport-pixel delta. |
reset |
() => boolean |
Restore defaultValue. |
fit |
() => boolean |
Scale and center the content inside the viewport. |
PanZoomStatus
Source: src/families/media/pan-zoom/pan-zoom-status.vue
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState & { readonly text: string } |
Announcement content. Defaults to the localized settled zoom level, updated when a gesture ends rather than on every frame. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered live region. |
text |
string |
Announced text. |
PanZoomViewport
Source: src/families/media/pan-zoom/pan-zoom-viewport.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name of the pan and zoom area. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the area. |
ariaDescribedby |
string |
undefined |
Space-separated ids of usage instructions (e.g. "Arrow keys pan, plus and minus zoom"). |
wheelSettleDelay |
number |
150 |
Quiet period after the last wheel event before transformEnd fires. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
PanZoomContent and overlays. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered viewport element. |
PanZoomZoomIn
Source: src/families/media/pan-zoom/pan-zoom-zoom-in.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name for icon-only content. Without slot content the localized messages.zoomIn label is rendered as text. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the action. Call preventDefault() to keep the transform. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Button content. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
disabled |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
PanZoomZoomOut
Source: src/families/media/pan-zoom/pan-zoom-zoom-out.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name for icon-only content. Without slot content the localized messages.zoomOut label is rendered as text. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the action. Call preventDefault() to keep the transform. |
Slots
| Slot |
Slot props |
Description |
default |
PanZoomSlotState |
Button content. Receives the current transform state. |
Exposed
| Member |
Type |
Description |
disabled |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Behavior
Normative state x input -> outcome table for pan-zoom-root.vue,
pan-zoom-viewport.vue, pan-zoom-content.vue, pan-zoom-zoom-in.vue,
pan-zoom-zoom-out.vue, pan-zoom-reset.vue, pan-zoom-fit.vue, and
pan-zoom-status.vue (@vizejs/ui/pan-zoom). Every row is proven by the named test.
The transform maps content to viewport pixels: viewport = content * scale + (x, y).
Every change passes through the scale limits and bounds before it is requested.
PanZoomContent applies transform: translate(x, y) scale(s) with
transform-origin: 0 0 inline (positioning mechanics) and publishes
--vize-ui-pan-zoom-x, --vize-ui-pan-zoom-y, and --vize-ui-pan-zoom-scale.
Consumers own the clipping box, e.g. [data-vize-ui="pan-zoom-viewport"] { overflow: hidden }.
| ID |
State |
Input |
Outcome |
Evidence |
| PZ1 |
idle |
render / mount |
focusable role="group" viewport with localized role description, inline transform + custom properties, labelled controls, polite status |
renders a labelled focusable viewport, transformed content, controls, and live status |
| PZ2 |
idle |
one pointer drag |
pans by the pointer delta (pointer), data-state="panning", emits transformStart/transformEnd; secondary mouse buttons are ignored |
single-pointer drags pan with start and end events |
| PZ3 |
two pointers |
spread / move / release one |
scales by the distance ratio around the midpoint (pinch), then continues as a pan from the remaining pointer |
two pointers pinch around their midpoint and fall back to panning |
| PZ4 |
wheelMode="zoom" |
wheel / ctrl-wheel |
zooms around the cursor (line and page deltas normalized); trackpad pinches use a higher sensitivity; one start/end pair per burst |
wheel zooms around the cursor, trackpad pinches zoom, and bursts settle once |
| PZ5 |
other wheel modes |
wheel |
pan scrolls the content (ctrl still zooms); zoom-with-ctrl leaves unmodified wheels to the page |
wheel modes pan or defer to page scrolling without modifiers |
| PZ6 |
doubleClickZoom |
double-click (+Shift) |
zooms one step in (out) around the pointer; disabled by doubleClickZoom=false |
double-click zooms in one step at the pointer and Shift zooms out |
| PZ7 |
focused viewport |
arrows / + = - / 0 / Home |
arrows pan by panStep (Shift ×4), zoom keys zoom around the center, 0 resets, Home fits; keys from content or with modifiers are ignored |
keyboard pans, zooms around the center, resets, and fits |
| PZ8 |
controls |
click |
zoom one step around the center, reset, or fit; zoom buttons disable at the effective limits; the status announces the settled level |
buttons zoom by one step, reset, fit, and disable at the scale limits |
| PZ9 |
bounds |
any change |
contain centers smaller axes and blocks gaps; cover raises the minimum scale to cover the viewport |
bounds constrain every change |
| PZ10 |
controlled |
any change |
emits the requested transform while rendering the controlled one until the parent accepts it |
controlled transforms win until the parent accepts the request |
| PZ11 |
disabled |
pointer / wheel / keys / clicks |
every interaction is ignored, the viewport leaves the tab order, buttons disable; the imperative API still applies |
disabled roots ignore input but keep the imperative API |
| PZ12 |
controls |
click with preventDefault() |
the transform is unchanged |
controls honor preventDefault from click listeners |
| PZ13 |
messages |
render |
role description, button labels, and the zoom announcement are localized |
messages localize the role description, labels, and zoom announcement |
| PZ14 |
exposed instance |
imperative calls |
setTransform, zoomTo, zoomIn, zoomOut, panBy, reset, fit report whether the transform changed |
exposes typed state and imperative transform controls |
| PZ15 |
missing provider |
setup |
compound parts fail closed with the shared context diagnostic |
compound parts require a matching root provider |
| PZ16 |
transform math |
pure helpers |
zoom anchoring, clamping, contain/cover/region bounds, fit, wheel normalization, and pinch math are deterministic |
pan-zoom-transform.test.ts |
| PZ17 |
SSR |
isolated requests |
markup is byte-identical, includes the initial transform, and omits client-only touch-action |
renders byte-identical pan-zoom markup across isolated SSR requests |
| PZ18 |
SSR / hydration |
hydrate |
server markup hydrates without warnings or node replacement |
hydrates pan-zoom markup without warnings or node replacement |
| PZ19 |
types |
compile |
transforms, bounds, modes, sources, messages, and exposes are closed and read-only |
pan-zoom.types.test-d.ts |
Listeners, touch-action: none, and pointer capture are client-only. Momentum and a
minimap are intentionally out of scope.