Build a Pan and Zoom Image Viewer with Vanilla JavaScript

Pan and zoom is a useful feature for viewing images. We can zoom in the image to see more details and perform image editing.

Dynamsoft Document Viewer is an SDK for such a purpose, which provides a set of viewers for document images. In this article, we are going to demonstrate how to use it to pan and zoom an image. In addition, we will also explore how to implement it from scratch with JavaScript.

What you’ll build: A JavaScript image viewer that supports mouse-wheel zoom, touch pinch-to-zoom, and drag-to-pan — first with Dynamsoft Document Viewer SDK, then from scratch with vanilla JavaScript and DOM scrolling.

Key Takeaways

  • Vanilla JavaScript can implement pan and zoom on standard DOM images using scroll position offsets and the wheel event — no canvas required.
  • Dynamsoft Document Viewer provides a production-ready Edit Viewer component with built-in pan, zoom, and pinch-to-zoom that works on both desktop and mobile.
  • Three implementation strategies exist for image pan/zoom: absolute pixel positioning (simple, keeps scrollbars), CSS transforms (GPU-accelerated), and canvas rendering (highest customizability).
  • Pinch-to-zoom on touch devices requires tracking the distance between two touch points across touchmove events.

Common Developer Questions

  • How do I pan and zoom an image with vanilla JavaScript without a library?
  • How do I add pinch-to-zoom support for images on mobile web browsers?
  • What is the best JavaScript SDK for building a document image viewer with zoom?

Prerequisites

  • A modern web browser (Chrome, Firefox, Safari, or Edge).
  • Basic knowledge of HTML, CSS, and JavaScript.
  • Get a 30-day free trial license for Dynamsoft Document Viewer (needed for the SDK approach).

Pan and Zoom Images Using Dynamsoft Document Viewer

  1. Create a new HTML file with the following template.

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
      <title>Edit Viewer</title>
      <style>
      </style>
    </head>
    <body>
    </body>
    <script>
    </script>
    </html>
    
  2. Include Dynamsoft Document Viewer’s files.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/ddv.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/ddv.css">
    
  3. Initialize Dynamsoft Document Viewer with a license. You can apply for one here.

    Dynamsoft.DDV.Core.license = "LICENSE-KEY"; 
    Dynamsoft.DDV.Core.engineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/engine";// Lead to a folder containing the distributed WASM files
    await Dynamsoft.DDV.Core.init();
    
  4. Create a new document instance.

    const docManager = Dynamsoft.DDV.documentManager;
    const doc = docManager.createDocument();
    
  5. Create an instance of edit viewer, bind it to a container and use it to view the document we just created.

    HTML:

    <div id="viewer"></div>
    

    JavaScript:

    const uiConfig = {
      type: Dynamsoft.DDV.Elements.Layout,
      flexDirection: "column",
      className: "ddv-edit-viewer-mobile",
      children: [
        Dynamsoft.DDV.Elements.MainView // the view which is used to display the pages
      ],
    };
    editViewer = new Dynamsoft.DDV.EditViewer({
      uiConfig: uiConfig,
      container: document.getElementById("viewer")
    });
    
    editViewer.openDocument(doc.uid);
    

    CSS:

    #viewer {
      width: 320px;
      height: 480px;
    }
    
  6. Use input to select an image file and load it into the document instance so that we can use the edit viewer to view the file.

    HTML:

    <label>
      Select an image:
      <br/>
      <input type="file" id="files" name="files" onchange="filesSelected()"/>
    </label>
    

    JavaScript:

    async function filesSelected(){
      let filesInput = document.getElementById("files");
      let files = filesInput.files;
      if (files.length>0) {
        const file = files[0];
        const blob = await readFileAsBlob(file);
        await doc.loadSource(blob); // load image
      }
    }
    
    function readFileAsBlob(file){
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = async function(e){
          const response = await fetch(e.target.result);
          const blob = await response.blob();
          resolve(blob);
        };
        fileReader.onerror = function () {
          reject('oops, something went wrong.');
        };
        fileReader.readAsDataURL(file); 
      })
    }
    

We can then hold the control key and use the mouse wheel to zoom in and zoom out and pan the image. On mobile devices, we can pinch to zoom. You can see how the image follows the mouse position or the touch position.

Build a Vanilla JavaScript Pan and Zoom Image Viewer from Scratch

There are several ways to implement pan and zoom.

  1. Use absolute pixel values. It is easy to understand and can have scrollbars.
  2. Use CSS transform. It can use GPU for a better performance but it cannot keep the scrollbars.
  3. Use Canvas. It has high performance and great customizability. Dynamsoft Document Viewer uses this method.

In the following, we are going to use the first way for demonstration.

  1. Create a container as the viewer. It contains the image.

    <div id="viewer">
      <img id="image"/>
    </div>
    

    Styles:

    It uses flex layout to align the views.

    The CSS for the viewer:

    #viewer {
      width: 320px;
      height: 480px;
      padding: 10px;
      border: 1px solid black;
      overflow: auto;
      display: flex;
      align-items: center;
    }
    

    The CSS for the image:

    #image {
      margin: auto;
    }
    
  2. Load the selected image file. Fit the image’s width to the viewer.

    let currentPercent;
    async function filesSelected(){
      let filesInput = document.getElementById("files");
      let files = filesInput.files;
      if (files.length>0) {
        const file = files[0];
        const blob = await readFileAsBlob(file);
        const url = URL.createObjectURL(blob);
        loadImage(url);
      }
    }
       
    function loadImage(url){
      let img = document.getElementById("image");
      img.src = url;
      img.onload = function(){
        let viewer = document.getElementById("viewer");
        let percent = 1.0;
        resizeImage(percent);
      }
    }
    
    function resizeImage(percent){
      currentPercent = percent;
      let img = document.getElementById("image");
      let viewer = document.getElementById("viewer");
      let borderWidth = 1;
      let padding = 10;
      let ratio = img.naturalWidth/img.naturalHeight;
      let newWidth = (viewer.offsetWidth - borderWidth*2 - padding*2) * percent
      img.style.width = newWidth + "px";
      img.style.height = newWidth/ratio + "px";
    }
       
    function readFileAsBlob(file){
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = async function(e){
          const response = await fetch(e.target.result);
          const blob = await response.blob();
          resolve(blob);
        };
        fileReader.onerror = function () {
          reject('oops, something went wrong.');
        };
        fileReader.readAsDataURL(file); 
      })
    }
    
  3. Add a wheel event for zooming with the mouse with the control key pressed.

    img.addEventListener("wheel",function(e){
      if (e.ctrlKey || e.metaKey) {
        if (e.deltaY < 0) {
          zoom(true);
        }else{
          zoom(false);
        }
        e.preventDefault();
      }
    });
       
    function zoom(zoomin,percentOffset){
      let offset = percentOffset ?? 0.1;
      if (zoomin) {
        currentPercent = currentPercent + offset;
      }else{
        currentPercent = currentPercent - offset;
      }
      currentPercent = Math.max(0.1,currentPercent);
      resizeImage(currentPercent);
    }
    
  4. Add pointer events to implement pan with a mouse or a touchscreen.

    let downPoint;
    let downScrollPosition;
    img.addEventListener("pointerdown",function(e){
      previousDistance = undefined;
      downPoint = {x:e.clientX,y:e.clientY};
      downScrollPosition = {x:viewer.scrollLeft,y:viewer.scrollTop}
    });
    img.addEventListener("pointerup",function(e){
      downPoint = undefined;
    });
    img.addEventListener("pointermove",function(e){
      if (downPoint) {
        let offsetX = e.clientX - downPoint.x;
        let offsetY = e.clientY - downPoint.y;
        let newScrollLeft = downScrollPosition.x - offsetX;
        let newScrollTop = downScrollPosition.y - offsetY;
        viewer.scrollLeft = newScrollLeft;
        viewer.scrollTop = newScrollTop;
      }
    });
    
  5. Add a touch move event to support pinch to zoom. It calculates the distance between the two touches to know whether to zoom in or zoom out.

    img.addEventListener("touchmove",function(e){
      if (e.touches.length === 2) {
        const distance = getDistanceBetweenTwoTouches(e.touches[0],e.touches[1]);
        if (previousDistance) {
          if ((distance - previousDistance)>0) { //zoom
            zoom(true,0.02);
          }else{
            zoom(false,0.02);
          }
          previousDistance = distance;
        }else{
          previousDistance = distance;
        }
      }
      e.preventDefault();
    });
       
    function getDistanceBetweenTwoTouches(touch1,touch2){
      const offsetX = touch1.clientX - touch2.clientX;
      const offsetY = touch1.clientY - touch2.clientY;
      const distance = offsetX * offsetX + offsetY + offsetY;
      return distance;
    }
    

All right, we’ve implemented the pan and zoom feature with JavaScript.

Common Issues and Edge Cases

  • Zoom jumps past the image bounds: If the user scrolls the mouse wheel rapidly, currentPercent can drop below a usable threshold. The code clamps it at 0.1 (10%), but very large images may need a higher minimum to stay responsive.
  • Pinch-to-zoom fires pan simultaneously: On touch devices, a two-finger pinch can also trigger single-pointer pointermove events, causing the image to pan while zooming. Guard the pan handler with a check like if (e.touches && e.touches.length >= 2) return; to suppress panning during pinch gestures.
  • Image flickers or reflows on resize: Rapidly changing img.style.width and img.style.height can cause layout thrashing. Wrapping the resize in requestAnimationFrame batches the DOM writes and eliminates visible flicker.

Source Code

https://github.com/tony-xlh/document-viewer-samples/