Skip to content

@beoe/pan-zoom

Small JS library to add pan and zoom functionality to SVG (inline or image). It supports gestures for all types of devices:

intentionmousetrackpad/touchpadtouchscren
panclick + moveclick + movetwo finger drag
zoomCtrl + wheelpinchpinch
resetdouble clickdouble clickdouble tap
scrollwheeltwo finger dragone finger drag

Pay attention:

  • gestures intentionally selected to not interfere with the system’s default scroll gestures, to avoid “scroll traps”
  • all actions are available through gestures, so it works without UI. You can add UI, though. Library exposes methods for this, like pan(dx, dy) and zoom(scale)
  • Cmd + click - zoom in
  • Alt + click - zoom out
  • First double click (tap) - zoom in x2

Usage

There are two flavors:

  • Headless - without UI
  • Default UI

Headless

If you have container element in HTML:

import { PanZoom } from "@beoe/pan-zoom";
document.querySelectorAll(".beoe").forEach((container) => {
const element = container.firstElementChild;
if (!element) return;
new PanZoom({ element, container }).on();
});

If you don’t have container element in HTML:

import { PanZoom } from "@beoe/pan-zoom";
document.querySelectorAll("svg, img[src$='.svg' i]").forEach((element) => {
if (element.parentElement?.tagName === "PICTURE") {
element = element.parentElement;
}
const container = document.createElement("div");
container.className = "beoe";
element.replaceWith(container);
container.append(element);
new PanZoom({ element, container }).on();
});

Additionally following CSS is required:

.beoe {
overflow: hidden;
touch-action: pan-x pan-y;
user-select: none;
cursor: grab;
}
.beoe svg,
.beoe img {
/* need to center smaller images to fix bug in zoom functionality */
margin: auto;
display: block;
/* need to fit bigger images */
max-width: 100%;
height: auto;
}
.beoe img {
pointer-events: none;
}

Instance methods:

  • on() - adds event listeners
  • off() - removes event listeners
  • pan(dx, dy) - pans image
  • zoom(scale) - zooms image
  • reset() - resets pan and zoom values

Default UI

import "@beoe/pan-zoom/css/PanZoomUi.css";
import { PanZoomUi } from "@beoe/pan-zoom";
document.querySelectorAll(".beoe").forEach((container) => {
const element = container.firstElementChild;
if (!element) return;
new PanZoomUi({ element, container }).on();
});

Additionally, CSS to style UI required (for example with Tailwind):

.beoe .buttons {
@apply inline-flex;
}
.beoe .zoom-in {
@apply bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-l;
}
.beoe .reset {
@apply bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4;
}
.beoe .zoom-out {
@apply bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r;
}

You can configure HTML classes used by UI:

const classes = {
zoomIn: "zoom-in",
reset: "reset",
zoomOut: "zoom-out",
buttons: "buttons",
tsWarning: "touchscreen-warning",
tsWarningActive: "active",
};
new PanZoomUi({ element, container, classes });

and message with instructions for the touchscreen:

const message = "Use two fingers to pan and zoom";
new PanZoomUi({ element, container, message });

Pixelation in Safari

Be aware that some CSS will cause pixelation of SVG on zoom (bug in Safari), for example:

  • will-change: transform;
  • transform: matrix3d(...);
  • transition-property: transform; (it setles after animation, though)

TODO

  • Do not stretch images if they are smaller than viewport
  • Do not show PanZoom UI for small images
  • Prevent clicks on drag or pan
  • minimap and full-screen mode, like in reactflow
  • Create a Rehype plugin to wrap images in a container (<figure class="beoe"></figure>) to avoid creating it on the client side.