ZoomPan

3 parts

Demo

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  ViewportRoot,
  ViewportSurface,
  ViewportContent,
} from '@robonen/primitives/canvas/zoom-pan';

<ViewportRoot>
  <ViewportSurface />
  <ViewportContent />
</ViewportRoot>

API Reference

ViewportRoot

ViewportRoot
PropTypeDescription
viewport?Viewport | undefinedTwo-way bound value (v-model:viewport).
Emits
EventPayload
update:viewport[value: Viewport | undefined]

ViewportSurface

ViewportSurface

The clipping / interaction pane. Clips the content (`overflow: hidden`), disables native touch gestures (`touch-action: none`), reports its live bounding rect into the context as the screen origin for coordinate math, and hosts the wheel / pinch / drag / dblclick / keyboard pan-zoom layer (`useZoomPan`). Carries `role` / `tabindex` for the a11y keyboard layer and reflects gesture state via `data-panning` / `data-zooming` / `data-measured`. Rendered by `ViewportRoot` by default; place directly only when composing the parts manually.

No props or events — renders its element and forwards attributes.

ViewportContent

ViewportContent

The single transformed layer. Everything a consumer renders inside the viewport lives here, so pan/zoom is one GPU-composited `transform` rather than a per-element restyle. `transform-origin: 0 0` is required — the coordinate formulas in `utils.ts` assume top-left scaling. `will-change: transform` is toggled on ONLY while interacting (never permanently): a pinned hint locks the compositor's raster scale, so the cached texture is GPU-upscaled and the content blurs at high zoom — toggling it lets the layer re-rasterise crisply once motion settles (see `useInteractionState`). Pure presentational; renders the default slot.

No props or events — renders its element and forwards attributes.