Lightweight JavaScript Pinch Zoom Library


Add pinch and wheel zoom with built-in drag to image viewers, maps, diagrams, and mobile-first layouts in plain HTML and JavaScript. hcg-pinch-zoom is a zero-dependency library. It supports touch pinch (two fingers), Safari trackpad gesture events, touchpad pinch (wheel + ctrlKey), Ctrl + mouse wheel, and drag while zoomed - without React, jQuery, or a heavy zoom toolkit.

On this page you will find a live demo, installation (direct download, npm, CDN), a full options and API reference with copy-paste examples, React usage notes, and styling guidance for image viewers and diagrams.

What is hcg-pinch-zoom?

hcg-pinch-zoom is a lightweight vanilla JavaScript library for zooming content inside a viewport. Call hcgPinchZoom() on a container element (or add data-hcg-pinch-zoom for auto-init). The first child becomes the transform target unless you pass a content selector. Zoom stays centered on the pinch midpoint or wheel cursor. Built-in drag is enabled when scale is not initialScale (default 100%).

hcg-pinch-zoom examples: touch pinch, Ctrl + wheel zoom, and pan while zoomed
hcg-pinch-zoom examples: touch pinch, Ctrl + wheel zoom, and pan while zoomed.

Why use hcg-pinch-zoom?

Most zoom libraries are tied to React or Vue, bundle tiled maps and canvas rendering you do not need, or leave you wiring touch pinch, Ctrl+wheel, focal-point math, and pan-by-drag yourself on top of raw CSS transforms.

hcg-pinch-zoom focuses on one job: pinch, wheel, and drag inside a viewport on plain HTML pages. It is zero-dependency, matches the API style of other HTML Code Generator libraries such as hcg-draggable and hcg-resizable, and suits image viewers, floor plans, product zoom, and mobile-first layouts without a heavy toolkit.

See the zoom library comparison for how hcg-pinch-zoom differs from CSS-only approaches and heavier zoom libraries, and the full capability list for everything included.

Demo

Pinch with two fingers on touch, or hold Ctrl and scroll on desktop. Zoom in, then drag to pan. Use Reset to return to 100% zoom or Disable to toggle gestures. For every option on one page, open the hcg-pinch-zoom demo page.

Sample image for pinch zoom demo

Scale: 1.00

Comparison Table

How hcg-pinch-zoom compares with common alternatives.

Feature hcg-pinch-zoom CSS transform only Heavy zoom libraries
DependenciesNoneNoneOften jQuery or a framework
Touch pinchYesManual event codeYes
Trackpad / Ctrl+wheelYesManualYes
Safari gesture eventsYesManualVaries
Pan while zoomedBuilt inSeparate drag codeUsually built in
Focal-point zoom mathYesYou implement itYes
Slider sync APIsetScale()ManualVaries
Data-attribute initYesNoSometimes
Approximate sizeOne small JS file plus CSSn/aLarge
License / costFree, MITn/aVaries

Choose hcg-pinch-zoom when you want a tiny vanilla helper for image viewers, maps, and diagrams. Use a heavier library when you need tiled imagery, physics, or a full canvas rendering pipeline.

Features

Everything included in the library at a glance:

  • Zero dependencies - one small JavaScript file and a companion CSS file.
  • Script tag or bundler - works with a plain script tag, CommonJS, or AMD.
  • Touch pinch zoom - two-finger gesture on phones and tablets.
  • Safari trackpad pinch - via gesturestart / gesturechange.
  • Trackpad pinch and Ctrl + wheel - real ctrlKey detection on wheel.
  • Built-in drag while zoomed - pan when scale !== initialScale.
  • Focal-point zoom - zoom toward cursor or pinch midpoint.
  • Scale limits - minScale, maxScale, and initialScale.
  • Slider sync - drive zoom from HTML range inputs with setScale().
  • Data-attribute init - data-hcg-pinch-zoom and data-hcg-* options.
  • Callbacks - onZoom and onPan for UI sync.
  • Static helpers - get(), destroy(), reset(), destroyAll().
  • Multiple instances - independent viewports on the same page.
  • React-friendly - create in useEffect and call destroy() on unmount.

Installation

Direct download

Source code and package:

Download hcg-pinch-zoom.js and hcg-pinch-zoom.css, then include both on your page:

HTML
<link rel="stylesheet" href="hcg-pinch-zoom.css">
<script src="hcg-pinch-zoom.js"></script>
npm

Install from npm:

Command Line
npm install hcg-pinch-zoom

CommonJS (require):

JavaScript
const hcgPinchZoom = require("hcg-pinch-zoom");
// also load hcg-pinch-zoom.css however your bundler handles CSS

ESM (import):

JavaScript
import hcgPinchZoom from "hcg-pinch-zoom";
import "hcg-pinch-zoom/hcg-pinch-zoom.css";
CDN usage

Load from a CDN with one script and one stylesheet - no npm or bundler required. The global hcgPinchZoom function is available as soon as the script finishes loading.

jsDelivr:

HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hcg-pinch-zoom@1/hcg-pinch-zoom.css">
<script src="https://cdn.jsdelivr.net/npm/hcg-pinch-zoom@1/hcg-pinch-zoom.js"></script>

unpkg:

HTML
<link rel="stylesheet" href="https://unpkg.com/hcg-pinch-zoom@1/hcg-pinch-zoom.css">
<script src="https://unpkg.com/hcg-pinch-zoom@1/hcg-pinch-zoom.js"></script>

Basic Usage

Install hcg-pinch-zoom, wrap zoomable content in a viewport with overflow: hidden, then init with a data attribute or JavaScript.

Initialize the viewport

Include CSS and JS, add a viewport element, and call hcgPinchZoom() or use data-hcg-pinch-zoom for auto-init.

HTML
<link rel="stylesheet" href="hcg-pinch-zoom.css">

<div class="viewport">
  <img src="photo.jpg" alt="Photo">
</div>

<script src="hcg-pinch-zoom.js"></script>
<script>
  const pz = hcgPinchZoom('.viewport', {
    minScale: 0.5,
    maxScale: 4,
    drag: true,
    onZoom(detail) {
      console.log('scale', detail.scale);
    }
  });
</script>
Data-attribute auto-init

Add data-hcg-pinch-zoom to the viewport. Options map to data-hcg-* attributes (see data-hcg attribute mapping).

HTML
<div data-hcg-pinch-zoom data-hcg-min-scale="0.5" data-hcg-max-scale="4">
  <img src="photo.jpg" alt="Photo">
</div>
Instance methods

Store the returned instance and call its methods whenever you need them.

JavaScript
const scale = pz.getScale();
pz.setScale(2);
pz.reset();
pz.disable();
pz.enable();
pz.destroy();
console.log(hcgPinchZoom.version);

Slider and Zoom Controls

Sync an HTML range slider with pinch zoom using setScale(). Use onZoom to update the slider when the user pinches or scrolls. Try it on the Slider zoom demo.

Range input sync
JavaScript
const viewport = document.querySelector('.viewport');
const slider = document.querySelector('#zoom-slider');

const pz = hcgPinchZoom(viewport, {
  minScale: 0.5,
  maxScale: 4,
  onZoom(detail) {
    slider.value = detail.scale;
  }
});

slider.addEventListener('input', () => {
  const scaleValue = Number(slider.value);
  const rect = viewport.getBoundingClientRect();
  pz.setScale(scaleValue, {
    clientX: rect.left + rect.width / 2,
    clientY: rect.top + rect.height / 2
  });
});
Reset without storing the instance

Use the static hcgPinchZoom.reset(selector) helper from toolbar buttons. See reset() method reference for the instance API.

Using with React

Import hcgPinchZoom from the package and create the viewport inside useEffect. Call destroy() in the cleanup function when the component unmounts.

Try the live React demo on StackBlitz

Zoom viewport component
JavaScript
import { useEffect, useRef } from "react";
import hcgPinchZoom from "hcg-pinch-zoom";
import "hcg-pinch-zoom/hcg-pinch-zoom.css";

function ZoomViewport({ src, alt }) {
  const ref = useRef(null);

  useEffect(() => {
    const instance = hcgPinchZoom(ref.current, {
      minScale: 0.5,
      maxScale: 4,
      onZoom(detail) {
        console.log(detail.scale);
      }
    });
    return () => instance.destroy();
  }, []);

  return (
    <div ref={ref} className="zoom-viewport">
      <img src={src} alt={alt} />
    </div>
  );
}

export default ZoomViewport;

Load hcg-pinch-zoom.css once in your app entry or import it in the component. Use disable() and enable() from props or effects when you need to lock zoom during async work.

Multiple Instances

Create as many zoom viewports on a page as you need. Each call to hcgPinchZoom() returns a fully independent instance with its own scale, position, options, and API.

HTML
<link rel="stylesheet" href="hcg-pinch-zoom.css">

<div id="photo-a" class="viewport"><img src="a.jpg" alt="Photo A"></div>
<div id="photo-b" class="viewport"><img src="b.jpg" alt="Photo B"></div>

<script src="hcg-pinch-zoom.js"></script>
<script>
  const zoomA = hcgPinchZoom(document.getElementById('photo-a'), { maxScale: 6 });
  const zoomB = hcgPinchZoom(document.getElementById('photo-b'), { maxScale: 3 });

  zoomA.reset();
  zoomB.getScale();
</script>
  • Link hcg-pinch-zoom.css once and initialize as many viewports as you need.
  • Static helpers accept a selector: hcgPinchZoom.reset('#photo-a').
  • Call each instance's destroy() when removing it from the DOM.

Styling and Customization

All default styles live in hcg-pinch-zoom.css, using plain single-class selectors. Edit that file directly, or override any rule from your own stylesheet loaded after it.

  • .hcg-pinch-zoom - the viewport (clips overflow, sets touch-action: none)
  • .hcg-pinch-zoom-content - the zoomable transform target
  • .hcg-pinch-zoom-draggable / .hcg-pinch-zoom-dragging - grab cursors while zoomed and during drag
  • .hcg-pinch-zoom-disabled - reduced opacity and no pointer events when disabled
CSS
.hcg-pinch-zoom { height: 320px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f9fafb; }
.hcg-pinch-zoom-content img { display: block; width: 100%; height: auto; pointer-events: none; }
.hcg-pinch-zoom-disabled { opacity: 0.5; }

Options

Copy-paste examples for each option. See the options types and defaults table for reference values.

content

CSS selector string or element to transform. Default: first child of the viewport, or the viewport itself if empty.

JavaScript
hcgPinchZoom('#viewport', {
  content: 'img'
});

hcgPinchZoom('#viewport', {
  content: document.querySelector('.zoom-target')
});
minScale

Minimum zoom level. Default: 0.5 (50%).

JavaScript
hcgPinchZoom('#viewport', {
  minScale: 0.5
});
HTML
<div data-hcg-pinch-zoom data-hcg-min-scale="0.5">
  <img src="photo.jpg" alt="Photo">
</div>
maxScale

Maximum zoom level. Default: 4 (400%).

JavaScript
hcgPinchZoom('#viewport', {
  maxScale: 8
});
initialScale

Starting zoom level and the scale at which drag is disabled. Default: 1 (100%). Position resets when scale equals initialScale.

JavaScript
hcgPinchZoom('#viewport', {
  initialScale: 1
});
wheel

Enable touchpad pinch and Ctrl+mouse-wheel zoom. Default: true. Plain scroll (no Ctrl) passes through.

JavaScript
hcgPinchZoom('#viewport', {
  wheel: true
});

pz.setOption('wheel', false);
pinch

Enable two-finger touch pinch and Safari gesture events. Default: true.

JavaScript
hcgPinchZoom('#viewport', {
  pinch: true
});

pz.setOption('pinch', false);
drag

Enable built-in drag while zoomed. Active only when scale !== initialScale. Default: true.

JavaScript
hcgPinchZoom('#viewport', {
  drag: true
});

hcgPinchZoom('#viewport', {
  drag: false   // zoom only, no pan
});
trackpadSensitivity

Exponential wheel zoom speed for trackpad pinch (ctrlKey without real Ctrl held). Default: 0.01.

JavaScript
hcgPinchZoom('#viewport', {
  trackpadSensitivity: 0.015
});
ctrlWheelSensitivity

Exponential wheel zoom speed when the user holds Ctrl and scrolls the mouse wheel. Default: 0.003.

JavaScript
hcgPinchZoom('#viewport', {
  ctrlWheelSensitivity: 0.005
});
maxWheelStep

Clamp each wheel deltaY (after normalization) to limit zoom speed per event. Default: 40.

JavaScript
hcgPinchZoom('#viewport', {
  maxWheelStep: 30
});
disabled

Start with zoom and drag disabled. Call enable() to activate. Default: false.

JavaScript
const pz = hcgPinchZoom('#viewport', { disabled: true });
pz.enable();
onZoom event

Called when scale changes (pinch, wheel, or setScale()). Not fired on initial setup. Receives { scale, x, y, target }.

onZoom event example

onPan event

Called while dragging the zoomed content. Receives { scale, x, y, target }.

onPan event example

Options Reference

Pass these as the second argument to hcgPinchZoom(element, options). Options can be changed at runtime with setOption(name, value).

OptionTypeDefaultDescription
contentstring | Elementfirst childCSS selector or element to transform. Defaults to the first child of the viewport, or the viewport itself if empty.
minScalenumber0.5Minimum zoom level (50%).
maxScalenumber4Maximum zoom level (400%).
initialScalenumber1Starting zoom and the scale at which drag is disabled. Position resets when scale equals this value.
trackpadSensitivitynumber0.01Multiplier for trackpad pinch (wheel with ctrlKey).
ctrlWheelSensitivitynumber0.003Multiplier for Ctrl + mouse wheel zoom.
maxWheelStepnumber40Maximum scale change per wheel event.
wheelbooleantrueEnable trackpad pinch and Ctrl + wheel zoom.
pinchbooleantrueEnable touch pinch and Safari gesture events.
dragbooleantrueEnable panning while zoomed.
disabledbooleanfalseStart disabled or toggle with disable() / enable().
onZoomfunction-Called when scale changes. Receives { scale, x, y, target }.
onPanfunction-Called while dragging zoomed content. Receives { scale, x, y, target }.

API Reference

hcgPinchZoom(element, options) returns an instance with these methods. The factory also exposes hcgPinchZoom.version, get(), destroy(), reset(), init(), and destroyAll().

MethodReturnsDescription
getScale()numberCurrent scale value.
setScale(scale, focal?)instanceApply zoom and update the DOM. Optional focal { clientX, clientY }.
getTransform()objectCurrent { x, y, scale }.
setTransform({ x, y, scale })instanceSet translate and scale together.
reset()instanceReset to initialScale and origin (0, 0).
enable()instanceTurn gestures on.
disable()instanceTurn gestures off and add .hcg-pinch-zoom-disabled.
setOption(name, value)instanceUpdate any option at runtime.
destroy()instanceRemove listeners, classes, and the instance reference.
hcgPinchZoom.get(target)instance | nullStatic helper to retrieve an instance by element or selector.
hcgPinchZoom.destroy(target)booleanStatic helper to destroy an instance.
hcgPinchZoom.reset(target)booleanStatic helper to reset an instance.
hcgPinchZoom.init(root?)voidScan for data-hcg-pinch-zoom elements.
hcgPinchZoom.destroyAll()voidDestroy every instance on the page.
getScale()

Return the current scale value.

JavaScript
const scale = pz.getScale();
setScale(scale, focal?)

Apply zoom and update the DOM. Optional focal point: { clientX, clientY } in screen coordinates. Without focal, zooms toward the viewport center.

JavaScript
pz.setScale(2);

pz.setScale(1.5, {
  clientX: 400,
  clientY: 300
});
getTransform() / setTransform({ x, y, scale })

Read or set translate and scale together.

JavaScript
const { x, y, scale } = pz.getTransform();

pz.setTransform({ x: 0, y: 0, scale: 1 });
reset()

Reset to initialScale and origin position (0, 0).

JavaScript
pz.reset();

// Without storing the instance:
hcgPinchZoom.reset('#viewport');
enable() / disable()

Turn gestures on or off at runtime.

JavaScript
pz.disable();
pz.enable();
setOption(name, value)

Update any option at runtime. Changing minScale, maxScale, or initialScale re-clamps the current scale.

JavaScript
pz.setOption('maxScale', 6);
pz.setOption('drag', false);
pz.setOption('onZoom', (detail) => console.log(detail.scale));
destroy()

Remove listeners, classes, and the instance reference from the element.

JavaScript
pz.destroy();

hcgPinchZoom.destroy('#viewport');   // static helper
Static: get(), init(), destroyAll(), version

hcgPinchZoom.get(target) returns the instance or null. init(root?) scans for data-hcg-pinch-zoom. destroyAll() destroys every instance.

JavaScript
const pz = hcgPinchZoom.get('#viewport');

hcgPinchZoom.init();
hcgPinchZoom.destroyAll();

console.log(hcgPinchZoom.version);

Data attributes

Every option that takes a simple value can be set in HTML. Mark the viewport with data-hcg-pinch-zoom; elements are initialized automatically on page load. Boolean and numeric options use data-hcg-* attributes as shown below.

AttributeMaps toExample
data-hcg-pinch-zoomauto-initdata-hcg-pinch-zoom
data-hcg-contentcontentdata-hcg-content="img"
data-hcg-min-scaleminScaledata-hcg-min-scale="0.5"
data-hcg-max-scalemaxScaledata-hcg-max-scale="4"
data-hcg-initial-scaleinitialScaledata-hcg-initial-scale="1"
data-hcg-wheelwheeldata-hcg-wheel="false"
data-hcg-pinchpinchdata-hcg-pinch="false"
data-hcg-dragdragdata-hcg-drag="false"
data-hcg-disableddisableddata-hcg-disabled="true"
HTML
<div
  data-hcg-pinch-zoom
  data-hcg-min-scale="0.5"
  data-hcg-max-scale="4"
  data-hcg-drag="true">
  <img src="photo.jpg" alt="Photo">
</div>

Callbacks

The library reports zoom and pan changes through onZoom and onPan options rather than custom DOM events. Pass them when you create the instance, or update them later with setOption().

onZoom

Fires when scale changes (pinch, wheel, or setScale()). Not fired on initial setup. Detail: { scale, x, y, target }. Use to sync HTML sliders, toolbar buttons, or a scale readout. For bidirectional slider sync, see the slider sync section.

JavaScript
const readout = document.querySelector('#zoom-readout');

const pz = hcgPinchZoom('#viewport', {
  onZoom(detail) {
    readout.textContent = Math.round(detail.scale * 100) + '%';
  }
});
onPan

Fires while dragging zoomed content. Detail: { scale, x, y, target }. x and y are translate values in pixels.

JavaScript
hcgPinchZoom('#viewport', {
  onPan(detail) {
    console.log('pan', detail.x, detail.y, 'scale', detail.scale);
  }
});
Change callbacks at runtime

Replace a callback after init with setOption().

JavaScript
pz.setOption('onZoom', (detail) => {
  console.log('New handler', detail.scale);
});

pz.setOption('onPan', (detail) => {
  console.log('Pan at', detail.x, detail.y);
});

Browser Support

hcg-pinch-zoom works in modern browsers that support touch events, pointer events, wheel listeners, and Safari gesture events where available.

BrowserSupported
Google ChromeYes
Mozilla FirefoxYes
Microsoft EdgeYes
Safari / iOS SafariYes
Mobile browsersYes (touch pinch and pan)

License

Released under the MIT License. Free for personal and commercial use. Copyright HTML Code Generator.

Frequently asked questions

Why does wheel zoom require Ctrl?

Browsers use Ctrl+wheel (or trackpad pinch) for page zoom. hcg-pinch-zoom listens for wheel events with ctrlKey so touchpad pinch and Ctrl+scroll zoom the content instead of the whole page. The handler calls preventDefault() to avoid browser zoom fighting content zoom.

How do I reset zoom without storing the instance?

Call hcgPinchZoom.reset('#viewport') or pass an element directly. It returns true when an instance was found. You can also use hcgPinchZoom.get(element)?.reset().

Does hcg-pinch-zoom work on iOS Safari?

Yes. Touch pinch uses touch events. Safari trackpad pinch uses gesturestart / gesturechange. The viewport sets touch-action: none so the browser does not scroll the page while you pinch or drag inside the zoom area.

Does hcg-pinch-zoom have any dependencies?

No. hcg-pinch-zoom is plain vanilla JavaScript with zero dependencies. Include hcg-pinch-zoom.js and hcg-pinch-zoom.css and you are ready to go.

Can I put clickable buttons or links inside zoomable content?

Yes. The library does not set pointer-events: none on content by default; only the disabled state (.hcg-pinch-zoom-disabled) blocks interaction. Demo styles often set pointer-events: none on images to prevent native image drag - remove that rule on elements that should be clickable.

button, a, input, and elements with data-hcg-pinch-zoom-interactive stay clickable while zoomed; the library skips pan on those targets. For custom clickable regions on plain div or span elements, add data-hcg-pinch-zoom-interactive or set drag: false for zoom-only mode.

Why does HTML text look blurry when zoomed?

hcg-pinch-zoom zooms with CSS transform: scale(). The default stylesheet sets will-change: transform on .hcg-pinch-zoom-content to keep pan and zoom smooth for photos, maps, and canvas. In Chrome and Edge that GPU layer hint can make HTML text look soft after zoom because the browser scales a cached bitmap instead of re-rasterizing glyphs. Firefox often stays sharp.

This is browser rendering behavior, not incorrect zoom math. Resizing the window forces a repaint and text may look sharp again. For HTML-heavy content, override in your CSS:

CSS
.hcg-pinch-zoom-content {
  will-change: auto;
}