---
title: "Drag-and-drop PDF viewer | Nutrient"
canonical_url: "https://www.nutrient.io/guides/web/samples/drag-and-drop/"
md_url: "https://www.nutrient.io/guides/web/samples/drag-and-drop.md"
last_updated: "2026-05-21T20:35:01.097Z"
description: "Learn to add custom views in Nutrient Web SDK, enabling drag-and-drop image elements onto document pages with our step-by-step code example."
---

# Drag-and-drop UI in our JavaScript PDF viewer

Add a custom view to the Nutrient Web SDK UI from which you can drag and drop image elements onto a document’s pages.

[Get Started](https://www.nutrient.io/sdk/web/getting-started.md)

[All Samples](https://www.nutrient.io/guides/web/samples.md)

[Download](https://www.nutrient.io/guides/web/downloads.md)

[Launch Demo](https://www.nutrient.io/demo/)

---

```js

import PSPDFKit from "@nutrient-sdk/viewer";
import * as React from "react";

// Assign the Nutrient Web SDK instance to a module variable so we can access it
// everywhere.
let instance = null;

// We track wether or not drag and drop is supported on the device. If not, we
// allow clicking an item to place it as well (e.g on iPhones)
let isDragAndDropSupported = false;

export function load(defaultConfiguration) {
  // We first find out how much space we have available. Based on that, we
  // decide wether to turn on the sidebar or not.
  const viewWidth = document.querySelector(".splitPane").getBoundingClientRect().width;

  // We start by initializing an initial ViewState that hides all toolbars,
  // opens the thumbnail sidebar, and places the sidebar on the other side.
  const initialViewState = new PSPDFKit.ViewState({
    showToolbar: false,
    enableAnnotationToolbar: false,
    sidebarMode: viewWidth > 1100? PSPDFKit.SidebarMode.THUMBNAILS : null,
    sidebarPlacement: PSPDFKit.SidebarPlacement.END,
  });

  // Initialize a new Web SDK Viewer with the initial view state and custom
  // stylesheets.
  return PSPDFKit.load({...defaultConfiguration,
    initialViewState,
    styleSheets: ["/drag-and-drop/static/style.css"],
    annotationTooltipCallback,
  }).then((_instance) => {
    instance = _instance;

    // get shadow root for instance.contentDocument
    const container = instance.contentDocument.host;

    // We only allow dropping elements onto a PDF page.
    container.ondragover = function (event) {
      isDragAndDropSupported = true;

      const actualTarget = event.composedPath()[0];

      const pageElement = closestByClass(actualTarget, "PSPDFKit-Page");

      if (pageElement) {
        // Allow drop operation
        event.preventDefault();
      }
    };

    container.ondrop = function (event) {
      isDragAndDropSupported = true;
      const actualTarget = event.composedPath()[0];

      const pageElement = closestByClass(actualTarget, "PSPDFKit-Page");

      if (pageElement) {
        const pageIndex = parseInt(pageElement.dataset.pageIndex);

        const isExternalDrop = event.dataTransfer.files.length > 0;

        if (isExternalDrop) {
          handleExternalDrop(event, pageIndex);
        } else {
          handleInternalDrop(event, pageIndex);
        }
      }
    };

    return instance;
  });
}

// Event handler that is called when a file from outside is dropped onto the PDF
// page.
function handleExternalDrop(event, pageIndex) {
  const file = event.dataTransfer.files[0];
  const allowedExternalMimeTypes = ["image/jpeg", "image/png"];

  if (!allowedExternalMimeTypes.includes(file.type)) {
    return;
  }

  const clientX = event.clientX;
  const clientY = event.clientY;

  // We don't know the dimensions of the image. To do this, we first parse it
  // with the use of this helper function. Note that it will run async so we
  // continue in the callback function.
  parseImageDimensions(file, (dimensions) => {
    const ratio = dimensions.height / dimensions.width;

    // External drag and drop items will have the cursor in the middle of the
    // bounding box.
    // We also scale the image so that the aspect ratio is kept.
    const width = 220;
    const height = width * ratio;

    const clientRect = new PSPDFKit.Geometry.Rect({
      left: clientX - width / 2,
      top: clientY - height / 2,
      width,
      height,
    });

    const pageRect = instance.transformContentClientToPageSpace(
      clientRect,
      pageIndex
    );

    insertImageAnnotation(pageRect, file, pageIndex);
  });

  event.preventDefault();
}

// Event handler that is called when an annotation from the internal toolbar is
// dropped onto a PDF page.
function handleInternalDrop(event, pageIndex) {
  // We know that internal drag and drop objects will have the cursor on the
  // top left left side of the box. We also know the dimensions of the
  // rectangles.
  const clientRect = new PSPDFKit.Geometry.Rect({
    left: event.clientX,
    top: event.clientY,
    width: 220,
    height: 217,
  });
  const pageRect = instance.transformContentClientToPageSpace(
    clientRect,
    pageIndex
  );

  // We generate text data with a string that either prefixes `pspdfkit/text` or
  // `pspdfkit/image`.
  const data = event.dataTransfer.getData("text");

  if (data.startsWith("pspdfkit/text")) {
    const text = data.replace("pspdfkit/text:", "");

    insertTextAnnotation(
      pageRect,
      text,
      pageIndex,
      26 / instance.currentZoomLevel
    );
    event.preventDefault();
  } else if (data.startsWith("pspdfkit/image")) {
    const imageUrl = data.replace("pspdfkit/image:", "");

    fetch(imageUrl).then((r) => r.blob()).then((blob) => insertImageAnnotation(pageRect, blob, pageIndex));
    event.preventDefault();
  }
}

// Event handler for preparing image drag and drop
function setDragImageData(event) {
  isDragAndDropSupported = true;
  event.dataTransfer.setData("text", "pspdfkit/image:" + event.target.src);
  event.dataTransfer.setDragImage &&
    event.dataTransfer.setDragImage(event.target, 0, 0);
  event.stopPropagation();
}

// Event handler for preparing text drag and drop
function setDragTextData(event) {
  isDragAndDropSupported = true;
  event.dataTransfer.setData("text", "pspdfkit/text:" + event.target.innerText);
  event.dataTransfer.setDragImage &&
    event.dataTransfer.setDragImage(event.target, 0, 0);
  event.stopPropagation();
}

// Handles click events on draggable image items on non draggable devices
function handleImageClick(event) {
  if (isDragAndDropSupported ||!instance) {
    return;
  }

  const target = event.target;

  fetch(target.src).then((r) => r.blob()).then((blob) => {
      const pageIndex = instance.viewState.currentPageIndex;
      const pageInfo = instance.pageInfoForIndex(pageIndex);

      insertImageAnnotation(
        new PSPDFKit.Geometry.Rect({
          width: target.width,
          height: target.height,
          left: pageInfo.width / 2 - target.width / 2,
          top: pageInfo.height / 2 - target.height / 2,
        }),
        blob,
        pageIndex
      );
    });
}

// Handles click events on draggable text items on non draggable devices
function handleTextClick(event) {
  if (isDragAndDropSupported ||!instance) {
    return;
  }

  const target = event.target;
  const pageIndex = instance.viewState.currentPageIndex;
  const pageInfo = instance.pageInfoForIndex(pageIndex);

  insertTextAnnotation(
    new PSPDFKit.Geometry.Rect({
      width: 220,
      height: 217,
      left: pageInfo.width / 2 - 220 / 2,
      top: pageInfo.height / 2 - 217 / 2,
    }),
    target.innerText,
    pageIndex,
    26
  );
}

// Inserts a text annotation on the page.
// https://www.nutrient.io/guides/web/annotations/introduction-to-annotations/
async function insertTextAnnotation(pageRect, text, pageIndex, fontSize) {
  const annotation = new PSPDFKit.Annotations.TextAnnotation({
    boundingBox: pageRect,
    text: {
      format: "plain",
      value: text,
    },
    pageIndex,
    fontSize,
    horizontalAlign: "center",
    verticalAlign: "center",
    backgroundColor: PSPDFKit.Color.WHITE,
  });

  await instance.create(annotation).then((annotations) => instance.setSelectedAnnotations(annotations[0]));
}

// Inserts an image annotation on the page.
// https://www.nutrient.io/guides/web/annotations/introduction-to-annotations/
async function insertImageAnnotation(pageRect, blob, pageIndex) {
  instance.createAttachment(blob).then((attachmentId) => {
    const annotation = new PSPDFKit.Annotations.ImageAnnotation({
      pageIndex,
      boundingBox: pageRect,
      contentType: "image/jpeg",
      imageAttachmentId: attachmentId,
    });

    instance.create(annotation).then((annotations) => {
      instance.setSelectedAnnotations(annotations[0]);
    });
  });
}

// The annotation tooltip can be used to place annotation tools directly on top
// of the annotation on screen.
//
// In this example, we use it as an alternative to the default annotation
// toolbars.
//
// https://web-examples.our.services.nutrient-powered.io/tooltips
function annotationTooltipCallback(annotation) {
  const deleteAnnotation = {
    type: "custom",
    title: "Delete",
    onPress: async () => {
      if (confirm("Do you really want to delete the annotation?")) {
        await instance.delete(annotation.id);
      }
    },
  };

  if (annotation instanceof PSPDFKit.Annotations.TextAnnotation) {
    const increaseFontSize = {
      type: "custom",
      title: "Bigger",
      onPress: async () => {
        annotation = annotation.set("fontSize", annotation.fontSize * 1.2);
        annotation =
          instance.calculateFittingTextAnnotationBoundingBox(annotation);

        await instance.update(annotation);
      },
    };

    const decreaseFontSize = {
      type: "custom",
      title: "Smaller",
      onPress: async () => {
        annotation = annotation.set("fontSize", annotation.fontSize / 1.2);
        annotation =
          instance.calculateFittingTextAnnotationBoundingBox(annotation);

        await instance.update(annotation);
      },
    };

    return [increaseFontSize, decreaseFontSize, deleteAnnotation];
  } else {
    return [deleteAnnotation];
  }
}

// Given a File object, we can create an <image/> tag to parse the image and
// retrieve the original dimensions.
function parseImageDimensions(file, onDimensions) {
  const url = URL.createObjectURL(file);
  const image = new Image();

  image.onerror = () => URL.revokeObjectURL(url);
  image.onload = () => {
    onDimensions({ width: image.width, height: image.height });
    URL.revokeObjectURL(url);
  };
  image.src = url;
}

const tools = [
  { type: "image", filename: "product1.jpg" },
  { type: "image", filename: "product2.jpg" },
  { type: "image", filename: "product3.jpg" },
  { type: "text", text: "Best Price" },
  { type: "text", text: "Top Service" },
];

// By exporting a CustomContainer, we can customize the HTML structure that is
// used by the catalog app.
// We do this so that we can show the sidebar and fill it with some example
// tools.
export const CustomContainer = React.forwardRef((instance, ref) => (
  <div className="splitPane">
    <div className="splitPane-left">
      {tools.map((tool) => {
        if (tool.type === "image") {
          return (
            <div key={tool.filename} className="image-tool tool">
              <img
                src={"/drag-and-drop/static/" + tool.filename}
                width="220"
                height="217"
                onDragStart={setDragImageData}
                onClick={handleImageClick}
                draggable
              />
            </div>
          );
        } else {
          return (
            <div
              key={tool.text}
              className="text-tool tool"
              onDragStart={setDragTextData}
              onClick={handleTextClick}
              draggable
            >
              {tool.text}
            </div>
          );
        }
      })}
    </div>
    <div className="splitPane-right" ref={ref} />

    <style jsx>{`.splitPane {
        width: 100%;
        height: 100%;
        background: #f6f8fa;

        display: flex;
      }.splitPane-left {
        background-color: rgb(250, 251, 251);
        padding: 10px;
      }.splitPane-right {
        height: 100%;
        flex-grow: 1;
      }.tool {
        margin: 10px;
      }.image-tool {
        display: block;
        cursor: pointer;
      }.image-tool img {
        outline: 2px solid #eee;

        outline-offset: -2px;
      }.text-tool {
        width: 220px;
        height: 217px;
        cursor: pointer;
        font-size: 26px;
        text-align: center;
        line-height: 217px;
        font-weight: bold;
        border: 2px solid #eee;

        color: #444;

        background: white;
      }

      @media only screen and (min-width: 768px) {.splitPane-left {
          width: 300px;
          height: 100vh;
          overflow-y: auto;
          -webkit-overflow-scrolling: touch;
          padding: 0 20px;
          box-shadow: 5px 0 5px rgba(200, 200, 200, 0.2);
        }.splitPane {
          flex-direction: row;
        }.tool {
          display: block;
        }
      }

      @media only screen and (max-width: 767px) {.splitPane-left {
          width: auto;
          overflow-y: hidden;
          overflow-x: auto;
          -webkit-overflow-scrolling: touch;
          padding: 0px;
          box-shadow: 5px 0 5px rgba(200, 200, 200, 0.2);
          white-space: nowrap;
        }.splitPane-right {
          height: calc(100% - 150px);
        }.splitPane {
          flex-direction: column;
        }.tool,.tool > img {
          width: 110px;
          height: 108px;
          display: inline-block;
        }.text-tool {
          font-size: 13px;
          line-height: 108px;
        }.tool {
          vertical-align: top;
        }
      }
    `}</style>
  </div>
));

function closestByClass(el, className) {
  return el && el.classList && el.classList.contains(className)? el
    : el? closestByClass(el.parentNode, className)
    : null;
}

```

This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.

---

## Related pages

- [Open, view, and annotate on images using JavaScript](/guides/web/samples/annotating-images.md)
- [Add watermarks to PDFs using JavaScript example](/guides/web/samples/add-watermarks-to-pdf-javascript.md)
- [Customizing PDF text search using JavaScript](/guides/web/samples/customized-pdf-search.md)
- [Customized Document Editor Toolbar](/guides/web/samples/customized-document-editor-toolbar.md)
- [Custom HTML PDF annotations using JavaScript](/guides/web/samples/custom-annotations.md)
- [Add electronic signature images to PDFs using JavaScript](/guides/web/samples/adding-image-electronic-signatures.md)
- [View PDFs in dark mode using JavaScript](/guides/web/samples/dark-mode-pdf-viewer.md)
- [Customize PDF annotation tooltips using JavaScript](/guides/web/samples/custom-annotation-tooltip.md)
- [Customize the PDF toolbar using JavaScript](/guides/web/samples/customized-pdf-toolbar.md)
- [Customize PDF annotation permissions using JavaScript](/guides/web/samples/custom-annotation-permissions.md)
- [Open PDFs using JavaScript](/guides/web/samples/open-pdf-using-javascript.md)
- [Edit PDFs using JavaScript](/guides/web/samples/edit-pdf-javascript.md)
- [Flipbook PDF viewer using JavaScript](/guides/web/samples/flipbook.md)
- [PDF presentation mode using JavaScript](/guides/web/samples/presentation-mode.md)
- [Handling password-protected PDFs in our JavaScript viewer](/guides/web/samples/password-protected-pdf.md)
- [Customizing JavaScript PDF printing modes](/guides/web/samples/pdf-printing-modes.md)
- [Add electronic signatures to PDFs using JavaScript](/guides/web/samples/electronic-signatures-in-pdf.md)
- [Create custom overlays on PDFs using JavaScript](/guides/web/samples/custom-overlay-items.md)
- [PDF form support using JavaScript](/guides/web/samples/javascript-pdf-form.md)
- [Disable PDF editing and annotations](/guides/web/samples/open-read-only-pdf.md)
- [Storing electronic signatures in the browser using JavaScript](/guides/web/samples/stored-electronic-signatures.md)
- [Zoom example for our JavaScript PDF viewer](/guides/web/samples/zooming.md)
- [PDF text selection using JavaScript](/guides/web/samples/pdf-text-selection-javascript.md)
- [PDF Collaboration permissions using JavaScript](/guides/web/samples/collaboration-permissions.md)
- [Collaborate on PDFs using JavaScript](/guides/web/samples/instant-pdf-collaboration.md)
- [Hide or reveal area on PDFs using JavaScript](/guides/web/samples/hide-reveal-area-in-pdf.md)
- [JavaScript PDF magazine viewer](/guides/web/samples/javascript-magazine-viewer.md)
- [Digitally sign a PDF using JavaScript](/guides/web/samples/javascript-digital-signatures.md)
- [PDF annotation in JavaScript](/guides/web/samples/javascript-pdf-annotations.md)
- [Redact PDFs using JavaScript](/guides/web/samples/javascript-pdf-redaction.md)
- [Customize the UI for PDF annotations using JavaScript](/guides/web/samples/annotations-inspector.md)

