webdevpuneet.com

Pinch & Scroll Zoom Image Viewer — Free Touch Pinch-Zoom Snippet

Pinch & Scroll Zoom Image Viewer · Media · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="pz-wrap">
  <div class="pz-stage" id="pzStage">
    <div class="pz-canvas" id="pzCanvas">
      <img class="pz-img" id="pzImg" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=900&q=70&auto=format&fit=crop" alt="Mountain landscape" draggable="false">
    </div>
    <div class="pz-badge" id="pzBadge">100%</div>
  </div>
  <div class="pz-bar">
    <p class="pz-hint">Pinch with two fingers, scroll to zoom, or drag while zoomed in</p>
    <button type="button" class="pz-reset" id="pzReset">Reset zoom</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real pinch detection
Two-finger distance ratio drives the actual scale.
Zoom-to-point math
The pinched or scrolled point stays visually fixed.
Wheel zoom parity
Desktop scroll mirrors the touch pinch behavior.
Clamped scale
MIN_SCALE/MAX_SCALE stop over- or under-zoom.
Bounded panning
Drag can't pull the image past its own edges.
One-finger and mouse pan
Both input types share the same clamp logic.
Live zoom badge
A percentage readout tracks the current scale.
One-tap reset
Instantly returns to 100% and centered.

About this UI Snippet

Pinch & Scroll Zoom Image Viewer — Real Finger-Distance Math

Screenshot of the Pinch & Scroll Zoom Image Viewer snippet rendered live

This viewer implements the actual pinch-to-zoom gesture you'd expect from a native photo app: two fingers on the glass, moving apart or together, scaling the image in real time — plus a mouse wheel equivalent for desktop and drag-to-pan once zoomed in. It's built in plain HTML, CSS, and vanilla JavaScript with no gesture library, tracking real touchstart/touchmove/touchend events and event.touches.

Finger-distance ratio, not a fake slider

When a second finger lands (e.touches.length === 2), the code records the starting distance between the two touch points with the Pythagorean theorem — Math.sqrt(dx*dx + dy*dy) — and the scale at that moment. On every subsequent touchmove, it recomputes the current distance and divides it by the starting distance: ratio = dist / pinch.startDist. Multiplying that ratio by the starting scale is the actual pinch math — fingers twice as far apart as when the gesture began means the image is scaled 2x from where it started. This is genuine two-finger tracking, not a decorative animation that plays regardless of finger movement.

Zooming toward a fixed point

Both the pinch and the wheel handler call a shared zoomAt(x, y, nextScale) that keeps whatever point is under the cursor (or the pinch midpoint) visually stationary as the scale changes. It does this by solving for the new translation: tx = px - (px - tx) * ratio, where ratio is the scale change. Without this, zooming would always scale around the image's top-left corner and the point you're actually pinching or scrolling over would drift away — the standard complaint with naive zoom implementations.

Wheel zoom on desktop

wheel events feed the same zoomAt function, converting deltaY into a small multiplicative scale change so scrolling up zooms in and down zooms out, at the cursor position — desktop users get the same "zoom toward what I'm pointing at" behavior touch users get from pinching.

Drag-to-pan and clamped bounds

Once scale > MIN_SCALE, both a single remaining touch and a mouse drag translate the image, with clampPan() constraining the translation so the image edges can never be dragged past the stage bounds — you can't pan into empty space. MIN_SCALE/MAX_SCALE similarly clamp every zoom operation (pinch, wheel, or the derived scale) so the image can neither shrink below its natural size nor blow up into a blurry mess.

Customizing it

Change MIN_SCALE/MAX_SCALE, swap the wheel sensitivity constant, or add double-tap-to-zoom by wiring a tap-timing check (see double-tap to like for that exact detection pattern) into a call to zoomAt. Pair it with an image magnifier for a hover-based alternative, or an image zoom card for a lighter-weight click-to-zoom.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn image renders inside a bounded, touch-friendly stage.
  2. 2
    Pinch with two fingersThe image scales in real time from the pinch midpoint.
  3. 3
    Scroll the mouse wheelDesktop zooms toward the cursor position.
  4. 4
    Drag while zoomed inPan around the enlarged image, clamped to its edges.
  5. 5
    Click Reset zoomInstantly returns to 100% and centered.
  6. 6
    Tune the clampsChange MIN_SCALE, MAX_SCALE, and wheel sensitivity.

Real-world uses

Common Use Cases

Product photo inspection
Let shoppers examine stitching or texture beyond the thumbnail, with two-finger pinch on touch and the mouse wheel on desktop.
Gallery lightbox zoom
Add real zoom and pan to a full-screen photo view, with clamped MIN_SCALE and MAX_SCALE preventing over-zoom or tiny images.
Map and diagram viewers
Pinch and pan large diagrams or floor plans, with zoom-to-point maths keeping the pinched spot visually fixed.
Scanned document previews
Zoom into scanned pages or receipts, using a reset button to return quickly to the fit-to-screen view.
Zoom style comparisons
Compare with an image magnifier or an image zoom card, which show detail in place rather than scaling the whole picture.
Related: Unsaved Changes Guard — Confirm Before Closing a Dirty Modal
See the Unsaved Changes Guard — Confirm Before Closing a Dirty Modal for a related modals pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

When a second finger touches down, the code measures the distance between the two touch points using the Pythagorean theorem and stores it along with the current scale. On every touchmove with two fingers still down, it recomputes that distance and divides by the starting distance to get a ratio, then multiplies the starting scale by that ratio. Fingers twice as far apart as when the gesture began means the image is at 2x its starting scale — genuine two-finger tracking, not a canned animation.

Both the pinch and wheel handlers call a shared zoomAt(x, y, scale) function that solves for the translation needed to keep the point under the cursor or pinch midpoint stationary as scale changes: tx = px - (px - tx) * ratio. Without this correction, every zoom would scale around the image's top-left corner, and whatever you were actually pinching or scrolling over would visibly slide away.

The wheel event handler converts the scroll delta into a small multiplicative scale change and passes the cursor's clientX/clientY into the exact same zoomAt function the pinch gesture uses, so desktop scrolling zooms toward the cursor position with identical clamping and point-anchoring behavior as the touch gesture.

clampPan() runs after every pan or zoom and constrains the translation so the scaled image's edges can never move past the stage's visible bounds — computed from the stage's actual rendered size and the current scale. This prevents dragging or zooming into empty space around the image.

Keep scale, tx, and ty in refs (not state) since they change on every touchmove/wheel frame, and apply them via a transform style bound to the canvas ref in an effect or directly via ref.current.style. Attach the touch, wheel, and mouse listeners in a mount effect with { passive: false } where preventDefault is needed, and clean them up on unmount.