How to Preview and Select Multiple Images in a JavaScript Document Viewer

During document scanning, we may need to select multiple images for reordering, retouching, export, etc. It is useful to have a document viewer which supports selecting multiple images.

Dynamsoft Document Viewer is an SDK for such a purpose. It provides a set of viewers for documents. In this article, we are going to demonstrate how to use it to browse and select multiple images. In addition, we will also explore how to implement it from scratch.

What you’ll build: A JavaScript image viewer that supports selecting multiple thumbnails via click, Ctrl+click, and Shift+click — first using the Dynamsoft Document Viewer SDK, then from scratch with plain HTML/CSS/JS.

Key Takeaways

  • Dynamsoft Document Viewer’s BrowseViewer provides built-in multi-select with Ctrl+click (toggle) and Shift+click (range) out of the box, using a high-performance Canvas renderer.
  • A custom multi-select viewer can be built from scratch using CSS flex layout, classList.toggle, and keyboard modifier detection (event.ctrlKey, event.shiftKey).
  • The SDK approach requires only ~20 lines of JavaScript to load images and enable thumbnail browsing with selection.
  • Multi-image selection is essential for document scanning workflows that involve reordering, batch export, or retouching.

Common Developer Questions

  • How do I select multiple images in a JavaScript viewer using Ctrl and Shift keys?
  • What is the easiest way to preview multiple image files in a browser with thumbnail selection?
  • How do I build a custom image thumbnail grid with multi-select support in plain JavaScript?

Prerequisites

To follow along with this tutorial, you need:

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

Browse and Select Multiple Images with 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>Browse 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 key (see Prerequisites above).

    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 browse viewer, bind it to a container and use it to view the document we just created.

    HTML:

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

    JavaScript:

    const browseViewer = new Dynamsoft.DDV.BrowseViewer({
      container: document.getElementById("viewer"),
    });
    
    browseViewer.openDocument(doc.uid);
    

    CSS:

    #viewer {
      width: 320px;
      height: 480px;
    }
    
  6. Use input to select multiple image files and load them into the document instance so that we can use the browse viewer to view the files.

    HTML:

    <label>
      Select images to load:
      <br/>
      <input type="file" id="files" name="files" multiple onchange="filesSelected()"/>
    </label>
    

    JavaScript:

    async function filesSelected(){
      let filesInput = document.getElementById("files");
      let files = filesInput.files;
      if (files.length>0) {
        for (let index = 0; index < files.length; index++) {
          const file = files[index];
          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 use hotkeys for multiple selection. Hold CTRL to select and unselect one image and hold SHIFT to select a range of images.

Browse Viewer uses Canvas internally which has a high performance.

Build a Custom Multi-Select Image Viewer from Scratch

So how does the multiple selection work? Let’s implement it in steps.

  1. Create a container as the viewer. It contains a list of thumbnail containers.

    <div id="viewer">
      <div class="thumbnail selected">
        <img src="blob:http://127.0.0.1:8000/d837ea7d-56aa-4d7c-baba-dde7503b72dd" style="height: 168px;">
      </div>
      <div class="thumbnail">
        <img src="blob:http://127.0.0.1:8000/62819aff-5f5b-4766-9b5c-92586bace2d6" style="height: 168px;">
      </div>
    </div>
    

    Styles:

    It uses flex layout to align the views.

    The CSS for the viewer:

    #viewer {
      display: flex;
      flex-wrap: wrap;
      align-content: flex-start;
      width: 320px;
      height: 480px;
      overflow: auto;
      background: lightgray;
      border: 1px solid black;
    }
    

    The CSS for the thumbnails:

    :root {
      --thumbnail-width: 140px;
    }
       
    .thumbnail {
      display: inline-flex;
      width: var(--thumbnail-width);
      height: 200px;
      padding: 5px;
      margin: 5px;
      align-items: center;
      justify-content: center;
    }
    
    .thumbnail:hover {
      background: gray;
    }
    
    .thumbnail.selected {
      background: gray;
    }
    
    .thumbnail.selected img {
      border: 1px solid orange;
    }
    
    .thumbnail img {
      width: 100%;
      max-height: 100%;
      object-fit: contain;
      border: 1px solid transparent;
    }
    
  2. Load images from selected files. The height of the image is adjusted based on the image ratio and its container’s width.

    let thumbnailWidth = 130;
    async function filesSelected(){
      let filesInput = document.getElementById("files");
      let files = filesInput.files;
      if (files.length>0) {
        for (let index = 0; index < files.length; index++) {
          const file = files[index];
          const blob = await readFileAsBlob(file);
          const url = URL.createObjectURL(blob);
          appendImage(url);
        }
      }
    }
    
    function appendImage(url){
      let viewer = document.getElementById("viewer");
      let thumbnailContainer = document.createElement("div");
      thumbnailContainer.className = "thumbnail";
      let img = document.createElement("img");
      img.src = url;
      img.onload = function(){
        let height = thumbnailWidth/(img.naturalWidth/img.naturalHeight);
        img.style.height = Math.floor(height) + "px";
      }
      thumbnailContainer.appendChild(img);
      viewer.appendChild(thumbnailContainer);
    }
    
  3. Adjust the thumbnail’s width based on whether the scrollbar is shown. Here we do this by changing a custom CSS property.

    function updateWidthBaseOnScrollBar(){
      let viewer = document.getElementById("viewer");
      if (viewer.scrollHeight>viewer.clientHeight) {
        let scrollBarWidth = viewer.offsetWidth - viewer.clientWidth;
        let width = 140 - Math.ceil(scrollBarWidth/2);
        document.documentElement.style.setProperty('--thumbnail-width', width + "px");
      }else{
        document.documentElement.style.setProperty('--thumbnail-width', "140px");
      }
    }
    
  4. Add click events for the thumbnails to select multiple images.

    thumbnailContainer.addEventListener("click",function(){
      const isMultiSelect = event.ctrlKey || event.metaKey;
      const isRangeSelect = event.shiftKey;
      const index = getIndex(thumbnailContainer);
      if (isMultiSelect) {
        toggleSelection(thumbnailContainer);
      } else if (isRangeSelect && lastSelectedIndex !== -1) {
        const firstSelectedIndex = getFirstSelectedIndex();
        if (firstSelectedIndex != -1) {
          selectRange(firstSelectedIndex, index);
        }else{
          selectRange(lastSelectedIndex, index);
        }
      } else {
        clearSelection();
        selectOne(thumbnailContainer);
      }
      lastSelectedIndex = index;
    })
    

    If no keys pressed, select only the clicked one.

    clearSelection();
    selectOne(thumbnailContainer);
       
    function selectOne(thumbnail) {
      thumbnail.classList.add('selected');
    }
       
    function clearSelection() {
      let thumbnails = document.querySelectorAll(".thumbnail");
      thumbnails.forEach(thumbnail => thumbnail.classList.remove('selected'));
    }
    

    If the CTRL key is pressed, toggle the selection status of the clicked one.

    function toggleSelection(thumbnail) {
      thumbnail.classList.toggle('selected');
    }
    

    If the SHIFT key is pressed, select images from the first selected index to the clicked index.

    const firstSelectedIndex = getFirstSelectedIndex();
    if (firstSelectedIndex != -1) {
      selectRange(firstSelectedIndex, index);
    }else{
      selectRange(lastSelectedIndex, index);
    }
       
    function selectRange(start, end) {
      let thumbnails = document.querySelectorAll(".thumbnail");
      clearSelection();
      const [startIndex, endIndex] = start < end ? [start, end] : [end, start];
      for (let i = startIndex; i <= endIndex; i++) {
        selectOne(thumbnails[i]);
      }
    }
    

Demo video:

Common Issues and Edge Cases

  • Scrollbar shifts thumbnail layout: When the number of images exceeds the viewer height and a scrollbar appears, the available width shrinks. The custom implementation above handles this by dynamically adjusting the CSS --thumbnail-width variable via updateWidthBaseOnScrollBar().
  • Large image files cause slow loading: Loading many high-resolution images as blobs can be slow. Consider generating smaller thumbnail previews or using createImageBitmap() for better performance.
  • Shift+click range depends on selection order: If no image is currently selected when Shift+click is used, the range falls back to the last clicked index. Ensure lastSelectedIndex is always tracked to avoid unexpected selection behavior.

Source Code

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