---
title: "How to build sticky note annotations with PDF.js and React"
canonical_url: "https://www.nutrient.io/blog/pdfjs-sticky-note-annotations/"
md_url: "https://www.nutrient.io/blog/pdfjs-sticky-note-annotations.md"
last_updated: "2026-06-26T11:02:24.400Z"
description: "Learn how to implement sticky note annotations in a PDF.js viewer using React. This tutorial covers click-to-place creation, coordinate conversion, drag-and-drop repositioning, and click vs. drag detection."
---

**TL;DR**

Sticky notes are point-based annotations — a small icon at one `[x, y]` location on a PDF page. Building them on PDF.js has four moving parts:

- **Click-to-place** — Convert the click’s canvas coordinates to PDF coordinates with `viewport.convertToPdfPoint(x, y)`.

- **Render** — Convert PDF coordinates back to screen space with `viewport.convertToViewportPoint(x, y)` and place a draggable element there.

- **Drag-to-move** — Use `react-rnd` (with `enableResizing={false}`) and recompute PDF coordinates from the drop position.

- **Click vs. drag** — Use a five-pixel threshold to distinguish opening the note from moving it.

If you’d rather not build this from scratch, [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) ships `NutrientViewer.Annotations.NoteAnnotation` with built-in comment threads, mentions, and real-time synchronization.

This tutorial references several application-specific helpers (`getPageFromElement`, `saveAnnotation`, `openAnnotationPanel`, `StickyNoteIcon`, and `useState` setters like `setNoteCreateAnchor`). Treat them as integration points with your own state and UI — none are part of PDF.js.

Sticky notes are point-based annotations — a small icon placed at a specific location on a PDF page. Unlike text highlights (rectangles) or area annotations (regions), notes store just a single coordinate pair.

## Data shape

A sticky note is stored as a plain object with five fields:

```ts

{
  type: "note",
  page: 3,              // 1-indexed page number.
  coords: [215.5, 680], // `[x, y]` in PDF coordinate space.
  color: "yellow",
  comments: [...]
}

```

## Creating a note: Click to place

When the note tool is active, a click on the page creates a note at that position:

```tsx

const handlePointerEnd = (startElement, origins) => {
  const page = getPageFromElement(startElement);
  if (!page) return;

  // Use the page div (not just the canvas wrapper) — it accounts for borders
  // and matches what PDF.js's own annotation layer uses.
  const pageView = viewer.getPageView(page.number - 1);
  const pageBounds = pageView.div.getBoundingClientRect();
  const canvasLeft = origins.clientOrigin[0] - pageBounds.left;
  const canvasTop = origins.clientOrigin[1] - pageBounds.top;

  const [pdfX, pdfY] = pageView.viewport.convertToPdfPoint(canvasLeft, canvasTop);

  // Show a popover for the user to enter note content.
  setNoteCreateAnchor({ left: origins.clientOrigin[0], top: origins.clientOrigin[1] });
  setNoteCreatePage(page.number);
  setNoteCreateCoords([pdfX, pdfY]);
};

```

## Rendering notes

The following component converts the stored PDF point to screen coordinates and renders a draggable icon:

```tsx

import React from "react";
import { Rnd } from "react-rnd";

function NoteAnnotation({ annotation, viewport, saveAnnotation, openAnnotationPanel }) {
  const [screenX, screenY] = viewport.convertToViewportPoint(
    annotation.coords[0],
    annotation.coords[1],
  );

  // Track the latest rendered position to distinguish click from drag.
  const positionRef = React.useRef({ x: screenX, y: screenY });
  positionRef.current = { x: screenX, y: screenY };

  const handleDragStop = (_, data) => {
    const dx = Math.abs(positionRef.current.x - data.x);
    const dy = Math.abs(positionRef.current.y - data.y);

    if (dx > 5 || dy > 5) {
      // Drag — save the new position in PDF coordinates.
      const [pdfX, pdfY] = viewport.convertToPdfPoint(data.x, data.y);
      saveAnnotation(annotation.id, { coords: [pdfX, pdfY] });
    } else {
      // Click — open the note panel. The next render restores position via props.
      openAnnotationPanel(annotation.id);
    }
  };

  return (
    <Rnd
      position={{ x: screenX, y: screenY }}
      size={{ width: 32, height: 32 }}
      enableResizing={false}
      onDragStop={handleDragStop}
      style={{
        position: "absolute",
        background: "#FFF3B0",

        borderRadius: "8px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        cursor: "pointer",
      }}
    >
      <StickyNoteIcon size={24} />
    </Rnd>
  );
}

```

Driving `Rnd` with `position` and `size` props (instead of the imperative `updatePosition`/`updateSize` ref methods) keeps the component fully declarative — when `viewport` changes, the parent rerenders and `Rnd` follows.

## Coordinate conversion for points

Notes use single-point conversion instead of rectangle conversion:

```tsx

// PDF -> Screen (for rendering).
const [screenX, screenY] = viewport.convertToViewportPoint(pdfX, pdfY);

// Screen -> PDF (for saving).
const [pdfX, pdfY] = viewport.convertToPdfPoint(screenX, screenY);

```

This is simpler than area/text annotations, which need `convertToViewportRectangle`.

## Click vs. drag detection

A common user experience issue: Clicking a note to view it also fires `onDragStop` because `react-rnd` treats every `pointerup` as the end of a drag. The fix is a small distance threshold — if the cursor moved less than five pixels between `pointerdown` and `pointerup`, treat it as a click. The `handleDragStop` in the previous snippet shows this pattern: Compute `dx` and `dy` from `positionRef.current`, branch on a five-pixel threshold, save the new position on drag, or open the note panel on click.

When the threshold isn’t exceeded, the parent rerenders with the original `position` prop, and `Rnd` automatically snaps back to the saved coordinates — no imperative reset needed.

## Cursor styles

Use CSS and a class toggle to change the cursor when the note tool is active:

```css.highlightNoteCursor.pdfViewer.page {
  cursor: crosshair;
}

```

```tsx

viewer.container.classList.toggle("highlightNoteCursor", tool === "note");

```

## Key points

- Notes store a single `[x, y]` point in PDF coordinates, not a rectangle.

- Use `viewport.convertToViewportPoint`/`convertToPdfPoint` for point conversion.

- `react-rnd` with `enableResizing={false}` gives you a drag-only element.

- Use a pixel threshold to distinguish clicks from drags.

- Drive `Rnd` with `position` and `size` props so it rerenders declaratively on viewport changes.

- Notes are the simplest annotation type — a good starting point before building text/area annotations.

## FAQ

#### Why store sticky notes as a single point instead of a rectangle?

A sticky note’s icon doesn’t grow with zoom — it’s a fixed-size affordance that always renders at one position on the page. Storing only `[x, y]` means you don’t have to encode arbitrary width/height that the renderer would then have to ignore. Text highlights and area annotations are rectangular because their geometry _is_ semantically meaningful.

#### What’s the difference between <code>convertToPdfPoint</code> and <code>convertToViewportPoint</code>?

`convertToPdfPoint(screenX, screenY)` converts screen → PDF coordinates (use when saving a user-initiated position). `convertToViewportPoint(pdfX, pdfY)` converts PDF → screen coordinates (use when rendering a stored annotation). Both are methods on `pageView.viewport`, and they invert each other.

#### Why use a click-vs.-drag threshold?

`react-rnd` fires `onDragStop` on every `pointerup`, even when the user only clicked. Without a threshold, every click would save the same coordinates back to your store (a no-operation save, but it triggers your dirty-state logic). The five-pixel threshold matches the operating system–level click tolerance most operating systems use to dismiss accidental drags.

#### Why use <code>pageView.div</code> and not <code>.canvasWrapper</code> for the bounding rect?

`pageView.div` is the canonical bounding box of the page in the PDF.js viewer — it includes the canvas, text layer, and annotation layer. `.canvasWrapper` is a child `div` that PDF.js manages internally; its bounds usually match `pageView.div`, but borders and CSS transforms can shift them. Using `pageView.div` matches what PDF.js’s own annotation layer uses for coordinate math.

#### How do I clear the note tool after placing a note?

The post toggles a `highlightNoteCursor` class on `viewer.container` when the tool is active. To clear it, set `tool` back to your default (e.g. `"select"`) and remove the class. Many implementations also clear the active tool after a single placement so users don’t accidentally drop a second note.

#### How does Nutrient Web SDK compare for sticky notes?

Nutrient ships `NutrientViewer.Annotations.NoteAnnotation` with built-in comment threads, mentions, color picker, and real-time synchronization. There’s no manual coordinate conversion, no `react-rnd`, and no click-vs.-drag detection — the SDK handles all of it. See the [annotations guide](https://www.nutrient.io/guides/web/annotations/introduction-to-annotations.md) and the [migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md).

## How Nutrient Web SDK handles this

Nutrient note annotations require no click-vs.-drag detection, no coordinate conversion, and no imperative position updates on zoom — they’re interactive by default:

```js

const annotation = new NutrientViewer.Annotations.NoteAnnotation({
  pageIndex: 2,
  boundingBox: new NutrientViewer.Geometry.Rect({
    left: 215,
    top: 680,
    width: 32,
    height: 32,
  }),
  text: { value: "Review this section" },
  color: new NutrientViewer.Color({ r: 255, g: 243, b: 176 }),
});
await instance.create(annotation);

```

For threaded discussion on top of any annotation, see [Comments and Replies](https://www.nutrient.io/guides/web/annotations/comments-and-replies/comments.md) — a separate Nutrient component with @mentions and real-time synchronization.

---

_See [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) for built-in note annotations, or follow the [migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) to switch from PDF.js. [Talk to Sales](https://www.nutrient.io/contact-sales/?=sdk) about your requirements._
---

## Related pages

- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Viewer](/blog/document-viewer.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [How To Build A Powerpoint Viewer Using Javascript](/blog/how-to-build-a-powerpoint-viewer-using-javascript.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [or](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [or](/blog/how-to-convert-html-to-pdf-using-react.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [How To Generate Pdf From Html With Nodejs](/blog/how-to-generate-pdf-from-html-with-nodejs.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [From an HTML string.](/blog/html-in-pdf-format.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdfjs Area Annotations Canvas Capture](/blog/pdfjs-area-annotations-canvas-capture.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Annotation Editor Layer](/blog/pdfjs-annotation-editor-layer.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Process Flows](/blog/process-flows.md)
- [or](/blog/sample-blog-updated.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Pdfium Is A Trusted Platform For Pdf Rendering](/blog/why-pdfium-is-a-trusted-platform-for-pdf-rendering.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)

