---
title: "How to add PDF page navigation, zoom, and rotation controls with PDF.js"
canonical_url: "https://www.nutrient.io/blog/pdfjs-navigation-zoom-rotation/"
md_url: "https://www.nutrient.io/blog/pdfjs-navigation-zoom-rotation.md"
last_updated: "2026-06-16T15:05:17.295Z"
description: "Learn how to build a toolbar for PDF.js with page navigation, zoom controls, and rotation. This tutorial covers PDFViewer APIs, EventBus listeners, and a complete React toolbar component."
---

**TL;DR**

Build a PDF.js toolbar with navigation, zoom, and rotation by operating on the `PDFViewer` instance and subscribing to `EventBus` events:

- **Page navigation** — `viewer.currentPageNumber`, `previousPage()`, `nextPage()`, listen for `pagechanging`

- **Zoom** — Assign string values (`"auto"`, `"page-width"`, `"1.5"`) to `viewer.currentScaleValue`, listen for `scalechanging`

- **Rotation** — Increment `viewer.pagesRotation` in 90-degree increments, listen for `rotationchanging`

- **Layout refresh** — Call `viewer.update()` on panel resizes

- **Precise scroll-to-position** — Use `PDFLinkService.goToDestination` with the `XYZ` destination type

If you’d rather skip the wiring, [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) ships a built-in toolbar and a typed `ViewState` API.

Building a toolbar for your PDF viewer means wiring up controls for page navigation, zoom, and rotation. All three operate through the `PDFViewer` instance and the `EventBus`.

## Page navigation

The `PDFViewer` instance tracks the current page and exposes methods to move between pages, while the `EventBus` reports page changes back to your UI.

### Reading current page

```tsx

const currentPage = viewer.currentPageNumber; // 1-indexed.
const totalPages = pdfDocument.numPages;

```

### Changing pages

```tsx

// Go to a specific page.
viewer.currentPageNumber = 5;

// Previous/Next.
viewer.previousPage();
viewer.nextPage();

```

### Listening for page changes

```tsx

eventBus.on("pagechanging", (evt) => {
  setCurrentPage(evt.pageNumber);
});

```

### Navigating to a specific location

Use `PDFLinkService.goToDestination` to scroll to an exact position on a page:

```tsx

linkService.goToDestination([
  pageIndex,         // 0-indexed page number.
  { name: "XYZ" },  // destination type.
  scrollLeft,        // x position (PDF coordinates), or null for current.
  scrollTop,         // y position (PDF coordinates), or null for current.
  null,              // zoom level, or null for current.
]);

```

This is the same mechanism PDF internal links use to scroll to a target location.

### React page navigation component

The component below assumes a `PDFContext` that exposes `viewer`, `eventBus`, and `pdfDocument` as references (typically populated when the PDF.js `PDFViewer` is initialized in a parent component).

```tsx

import { useContext, useEffect, useState } from "react";
import { PDFContext } from "./PDFContext";

function PageToolbar() {
  const { viewer, eventBus, pdfDocument } = useContext(PDFContext);
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);

  useEffect(() => {
    if (pdfDocument.current) {
      setTotalPages(pdfDocument.current.numPages);
    }
  }, [pdfDocument]);

  useEffect(() => {
    const handler = (evt) => setCurrentPage(evt.pageNumber);
    eventBus.current?.on("pagechanging", handler);
    return () => eventBus.current?.off("pagechanging", handler);
  }, [eventBus]);

  return (
    <div>
      <button
        onClick={() => viewer.current?.previousPage()}
        disabled={currentPage <= 1}
      >
        Previous
      </button>
      <input
        type="number"
        value={currentPage}
        min={1}
        max={totalPages}
        onChange={(e) => {
          const page = parseInt(e.target.value, 10);
          if (page >= 1 && page <= totalPages) {
            viewer.current.currentPageNumber = page;
          }
        }}
      />
      <span>of {totalPages}</span>
      <button
        onClick={() => viewer.current?.nextPage()}
        disabled={currentPage >= totalPages}
      >
        Next
      </button>
    </div>
  );
}

```

## Zoom/scale

Zoom is controlled through `viewer.currentScaleValue`, which accepts both preset keywords and numeric values, and the `EventBus` emits a `scalechanging` event whenever the scale updates.

### Setting scale

```tsx

// Preset values (strings).
viewer.currentScaleValue = "auto";       // Fit to container.
viewer.currentScaleValue = "page-width"; // Fit width.
viewer.currentScaleValue = "page-fit";   // Fit entire page.

// Numeric values (as string).
viewer.currentScaleValue = "1.5";        // 150 percent zoom.
viewer.currentScaleValue = "0.75";       // 75 percent zoom.

```

### Zoom in/out

```tsx

function zoomIn(viewer) {
  const current = parseFloat(viewer.currentScaleValue) || 1;
  viewer.currentScaleValue = String(Math.min(current + 0.25, 5));
}

function zoomOut(viewer) {
  const current = parseFloat(viewer.currentScaleValue) || 1;
  viewer.currentScaleValue = String(Math.max(current - 0.25, 0.25));
}

```

### Listening for scale changes

```tsx

eventBus.on("scalechanging", (evt) => {
  setScale(evt.scale);       // numeric value.
  setPreset(evt.presetValue); // "auto", "page-width", etc.
});

```

## Rotation

The `viewer.pagesRotation` property holds the current rotation, which you change in 90-degree increments. Each change adds to the previous value, and the `EventBus` fires a `rotationchanging` event whenever the rotation updates.

### Rotating pages

```tsx

// Rotate 90 degrees clockwise.
viewer.pagesRotation += 90;

// Rotate 90 degrees counterclockwise.
viewer.pagesRotation -= 90;

```

`pagesRotation` is cumulative. Values are in degrees (0, 90, 180, 270).

### Listening for rotation changes

```tsx

eventBus.on("rotationchanging", (evt) => {
  setRotation(evt.pagesRotation);
});

```

## Force refresh on layout changes

If your PDF viewer is in a resizable panel (sidebar toggle, split view), the pages may not reflow automatically. Force a refresh by calling `viewer.update()`:

```tsx

function refreshViewer() {
  viewer.update();
}

// Call on panel resize, sidebar toggle, etc.
window.addEventListener("resize", refreshViewer);
// Or use a custom event.
window.addEventListener("refresh-pdf-viewer", refreshViewer);

```

## Combined toolbar example

```tsx

import { useContext, useEffect, useState } from "react";
import { PDFContext } from "./PDFContext";

function PDFToolbar() {
  const { viewer, eventBus, pdfDocument } = useContext(PDFContext);
  const [page, setPage] = useState(1);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(pdfDocument.current?.numPages || 0);
    const h = (e) => setPage(e.pageNumber);
    eventBus.current?.on("pagechanging", h);
    return () => eventBus.current?.off("pagechanging", h);
  }, [eventBus, pdfDocument]);

  return (
    <div className="pdf-toolbar">
      {/* Page Navigation */}
      <button onClick={() => viewer.current?.previousPage()}>Prev</button>
      <span>{page} / {total}</span>
      <button onClick={() => viewer.current?.nextPage()}>Next</button>

      {/* Zoom */}
      <button onClick={() => viewer.current && zoomOut(viewer.current)}>-</button>
      <button onClick={() => {
        if (viewer.current) viewer.current.currentScaleValue = "page-width";
      }}>Fit Width</button>
      <button onClick={() => viewer.current && zoomIn(viewer.current)}>+</button>

      {/* Rotation */}
      <button onClick={() => {
        if (viewer.current) viewer.current.pagesRotation -= 90;
      }}>Rotate Left</button>
      <button onClick={() => {
        if (viewer.current) viewer.current.pagesRotation += 90;
      }}>Rotate Right</button>
    </div>
  );
}

```

## Key points

- Page numbers are 1-indexed in `viewer.currentPageNumber` but 0-indexed in `goToDestination`

- Scale values are strings — even numeric values like `"1.5"`

- Rotation accumulates in degrees on `viewer.pagesRotation`

- Always listen to `EventBus` events to keep your UI in sync with the viewer state

- Call `viewer.update()` to force a layout refresh after panel resizes

- `goToDestination` with `"XYZ"` type gives you precise scroll-to-position control

## FAQ

#### Why is <code>viewer.currentPageNumber</code> 1-indexed but <code>goToDestination</code> 0-indexed?

`currentPageNumber` is the user-facing page number (matches “Page 1 of 10” in the UI). `goToDestination` takes the array form from the PDF specification, where the first element is a zero-based page index. The two indexing conventions are intentional but easy to mix up.

#### Why are scale values strings instead of numbers?

`viewer.currentScaleValue` accepts both preset keywords (`"auto"`, `"page-width"`, `"page-fit"`) and numeric zoom levels (`"1.5"` for 150 percent) as strings. Using a single string type lets PDF.js distinguish “fit-to-width” from a hardcoded zoom. Always pass strings — `viewer.currentScaleValue = 1.5` silently fails.

#### How do I force the viewer to rerender after a layout change?

Call `viewer.update()`. This remeasures the container and reflows the pages. Common triggers: sidebar toggles, split-pane resizes, fullscreen transitions, and dynamic CSS changes that affect the container’s dimensions.

#### Do I need to import PDF.js’s CSS for the toolbar to work?

You need `pdfjs-dist/web/pdf_viewer.css` for the text layer, annotation layer, and page layout. The toolbar is your own React component, so it uses your own styles. Without the PDF.js CSS, pages render, but text selection and annotations break.

#### How does Nutrient Web SDK compare for the same use case?

Nutrient ships a built-in toolbar with navigation, zoom, rotation, and search controls, plus a typed `ViewState` API for programmatic control. There’s no `EventBus`, no string-typed scale values, and no manual layout refresh. See the [migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) for switching from PDF.js.

## How Nutrient Web SDK handles this

Instead of wiring up `EventBus` listeners, parsing string-typed scale values, and manually refreshing layouts, Nutrient’s `ViewState` API handles navigation, zoom, and rotation declaratively:

```js

// Page navigation.
instance.setViewState((v) => v.set("currentPageIndex", 4));

// Zoom.
instance.setViewState((v) => v.set("zoom", "FIT_WIDTH"));

// Rotation.
instance.setViewState((v) =>
  v.set("pagesRotation", (v.pagesRotation + 90) % 360),
);

```

The built-in toolbar ships with navigation controls, and `ViewState` keeps the toolbar, thumbnails, and scroll position in sync automatically.

---

_See [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) for an alternative to PDF.js, or follow the [migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) to switch. [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)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Viewer](/blog/document-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.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)
- [Html In Pdf Format](/blog/html-in-pdf-format.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Process Flows](/blog/process-flows.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [or](/blog/sample-blog-updated.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)

