You Might Also Like
Pinch & Scroll Zoom Image Viewer — Free Touch Pinch-Zoom Snippet
Pinch & Scroll Zoom Image Viewer · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Pinch & Scroll Zoom Image Viewer — Real Finger-Distance Math

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.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to derive the pinch-distance math from scratch. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how measuring the distance between two touch points with the Pythagorean theorem, then dividing the current distance by the distance recorded when the pinch began, produces a scale ratio that genuinely tracks finger movement — and why zoomAt solves for a new translation rather than only changing the scale, to keep the pinched point visually fixed. The same assistant can help you optimize it, for instance asking whether the wheel handler's sensitivity constant should be adjusted for trackpad "pinch" gestures (which browsers report as ctrlKey wheel events) versus a physical mouse wheel. It's also useful for extending the viewer: ask it to add double-tap-to-zoom using tap-timing detection, momentum/inertia when releasing a drag, or a minimap indicator showing which portion of the zoomed image is currently visible. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "pinch & scroll zoom image viewer" in plain HTML, CSS, and JavaScript with real multi-touch pinch detection — no gesture library.
Requirements:
- An image inside a bounded, overflow-hidden stage container, rendered inside an inner wrapper element that receives all scale/translate transforms (never transform the image element or stage directly), with touch-action: none on the stage so gestures don't trigger page scroll/zoom.
- Real two-finger pinch-to-zoom: on touchstart with exactly two active touches (via event.touches), record the distance between the two touch points using the Pythagorean theorem and the current scale. On touchmove with two touches still down, recompute that distance, divide it by the starting distance to get a ratio, and multiply the starting scale by that ratio to get the new scale — this must be real finger-distance math, not a fixed animation or a simulated slider.
- A shared zoom-toward-a-point function used by both the pinch gesture and mouse wheel zoom: given a screen point and a target scale, it must solve for a new translation that keeps that exact point visually stationary as the scale changes (not just changing scale and letting the image drift).
- Mouse wheel support on desktop that calls the same zoom-toward-a-point function using the cursor position and the wheel's deltaY converted into a small scale multiplier, with preventDefault so the page itself doesn't scroll.
- Drag-to-pan once zoomed in past the minimum scale, supporting both a single remaining touch point and a mouse drag, translating the image by the pointer's movement delta.
- Clamp the scale between a minimum (the image's natural size) and a maximum, and clamp the pan translation on every pan/zoom so the image's edges can never be dragged or zoomed past the visible stage bounds.
- A visible zoom percentage readout that updates live, and a reset button that returns scale and translation to their initial values.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSAn image renders inside a bounded, touch-friendly stage.
- 2Pinch with two fingersThe image scales in real time from the pinch midpoint.
- 3Scroll the mouse wheelDesktop zooms toward the cursor position.
- 4Drag while zoomed inPan around the enlarged image, clamped to its edges.
- 5Click Reset zoomInstantly returns to 100% and centered.
- 6Tune the clampsChange MIN_SCALE, MAX_SCALE, and wheel sensitivity.
Real-world uses
Common Use Cases
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.