Skip to content

ZoomableContainer

Figma-style pannable and zoomable viewport. Drop any element inside and navigate it with gestures. Reserves half-viewport padding around content so you can scroll past its edges.

Gestures

InputAction
Scroll / 2-fingerPan
Shift + scrollPan horizontally
Ctrl/Cmd + scrollZoom at cursor
Trackpad pinchZoom at cursor
Middle-button dragPan anywhere
Space + dragPan anywhere
Drag on backgroundPan

Live Demo — Custom Divs

Scroll to pan, Ctrl/Cmd+scroll (or pinch) to zoom, Space+drag or middle-mouse to grab.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Images

Canvas Inside

A <canvas> child works like any other element — the browser scales its bitmap. For pixel-perfect rendering at any zoom, re-render the canvas on scale changes.

Usage

Basic

vue
<template>
  <orio-zoomable-container style="width: 100%; height: 600px;">
    <div style="width: 2000px; height: 1500px;">
      <!-- any content -->
    </div>
  </orio-zoomable-container>
</template>

With Image

vue
<template>
  <orio-zoomable-container :min-scale="0.5" :max-scale="10">
    <img src="/large-photo.jpg" alt="" />
  </orio-zoomable-container>
</template>

With Canvas

vue
<template>
  <orio-zoomable-container>
    <canvas ref="canvasEl" width="1600" height="1200" />
  </orio-zoomable-container>
</template>

Programmatic Control

vue
<template>
  <orio-zoomable-container ref="zoomRef">
    <div class="board">...</div>
  </orio-zoomable-container>
  <orio-button @click="zoomRef?.resetView()">Reset</orio-button>
</template>

<script setup>
import { useTemplateRef } from "vue";
const zoomRef = useTemplateRef("zoomRef");
</script>

Reacting To Zoom Via Slot Prop

Use the scoped slot to counter-scale overlays (e.g. keep labels at constant size):

vue
<template>
  <orio-zoomable-container v-slot="{ scale }">
    <div class="board">
      <div class="pin" :style="{ transform: `scale(${1 / scale})` }">
        Always same size
      </div>
    </div>
  </orio-zoomable-container>
</template>

Props

PropTypeDefaultDescription
minScalenumber0.1Lower zoom bound
maxScalenumber10Upper zoom bound
initialScalenumber1Zoom level on mount
zoomSpeednumber0.0015Wheel-delta multiplier for zoom
wheelPanSpeednumber1Wheel-delta multiplier for pan

Events

EventPayloadDescription
update:scalenumberFires on zoom change
update:translate(x: number, y)Fires on pan change

Slots

SlotPropsDescription
default{ scale, tx, ty }Content to transform

Exposed Methods

Access via ref:

MethodDescription
setScaleAt(scale, px, py)Zoom to scale anchored at a point
panBy(dx, dy)Pan by a pixel delta
centerWorld()Center the content in the viewport
resetView()Reset scale + recenter
scale, tx, tyReactive refs of current state

Notes

  • Viewport reserves half-its-axis-size of empty space around the content on every side — you can pan until the content edge reaches the viewport center.
  • transform: scale() is GPU-accelerated; text may appear soft at non-integer zoom levels but remains selectable and accessible.
  • position: fixed children escape the transform (browser limitation).
  • For world-space click coordinates: worldX = (clientX - rect.left - tx) / scale.