npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@trimlinea/vue-panzoom

v0.1.0

Published

Performant, zero-dependency pan and zoom library for Vue 3 — composable-first, touch and pointer support.

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-firstusePanzoom() 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-panzoom

Quick 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