# React PDF Viewer: Search, Highlights, Real Annotations

> Build a React PDF viewer on PDFium with search, thumbnails and highlights that export as real PDF annotations, and see how it compares to react-pdf and pdf.js.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-pdf-viewer

## TL;DR

A production React PDF viewer needs virtualized pages that stay sharp when zoomed, text search with a result counter, thumbnails, selectable text, and a layout that still works on a phone. If people mark up the document, the highlights should be plain JSON your app can store and also export into the file as real PDF annotations that Acrobat, Preview and Chrome can read. The Wingo UI PDF Viewer does this on PDFium compiled to WebAssembly, with the highlights controlled from React state.

You have a contract, an invoice or a research paper as a PDF, and your app has to show it. An `<iframe>` hands the job to the browser's built-in viewer, which you cannot style, translate or listen to, and it looks different in every browser. A React PDF viewer gives you that control back: your toolbar, your search, your events. The hard parts come after the first page renders: keeping a 200-page file fast, keeping text sharp at 300%, making it usable on a phone, and letting people highlight passages without losing them when the file leaves your app. This guide covers those parts with the Wingo UI [PDF Viewer](https://wingo-ui.com/components/pdf-viewer). We build Wingo UI, so we are not neutral; outside facts link to their sources.

## What should a React PDF viewer handle?

A viewer people work in needs seven things, and only the first page render is easy with any library:

1. **Virtualized pages.** Only the pages on screen render, so a long file opens as fast as a short one.
2. **Sharp zoom.** At high zoom a single canvas per page gets huge, so zoomed pages paint in tiles.
3. **Search** with every hit marked, a "2 of 5" counter and Enter to step through results.
4. **Thumbnails** to jump through the file, and page rotation that the download keeps.
5. **Selectable text**, so people can copy a clause instead of retyping it.
6. **A phone layout**: a swipe scrolls, a long press selects, panels slide over the page.
7. **Highlights and notes**, if people review the document, stored where your app can read them.

react-pdf, the most common starting point, leaves point 7 to you entirely.

## Which React PDF viewer libraries should I compare?

Compare react-pdf, react-pdf-highlighter, react-pdf-viewer, EmbedPDF and the Wingo UI PDF Viewer. Each sits on one of two engines: Mozilla's [pdf.js](https://github.com/mozilla/pdf.js) (JavaScript, Apache 2.0) or Google's [PDFium](https://pdfium.googlesource.com/pdfium/) (C++, the engine inside Chromium) compiled to WebAssembly. Facts are from each project's GitHub README and npm listing, as of October 2026.

| Option | Engine | License | What you get | Highlights |
|---|---|---|---|---|
| [react-pdf](https://github.com/wojtekmaj/react-pdf) | pdf.js | MIT | `Document`, `Page`, `Thumbnail` and `Outline` components with text and annotation layers; you build the toolbar and search | Not built in |
| [react-pdf-highlighter](https://github.com/agentcooper/react-pdf-highlighter) | pdf.js | MIT | Text and image highlights, scroll to a highlight | Kept in your app; the README does not describe writing them into the file |
| [react-pdf-viewer](https://github.com/react-pdf-viewer/react-pdf-viewer) | pdf.js (per its GitHub topics) | Commercial license, bought on its site | Search, thumbnails, rotation, printing and a default layout | A highlight plugin on npm; the repository was archived on March 25, 2026 |
| [EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer) | EmbedPDF Runtime, a PDFium fork | Apache 2.0 per its README (the 2.15.1 npm packages list MIT) | A drop-in viewer, or tree-shakable plugins to build your own UI | Highlight, sticky note, free text and ink annotations, per its README |
| [Wingo UI PDF Viewer](https://wingo-ui.com/components/pdf-viewer) | PDFium through the EmbedPDF plugins | Pro source component | Toolbar, search, thumbnails, rotation, upload, download, highlights panel | JSON in your state, exported as real PDF annotations |

If you want a react pdf viewer free to use in a commercial app and you will build the UI yourself, react-pdf is the well-trodden path. EmbedPDF is the better pick when you want annotations without paying for a component. Pick react-pdf-highlighter when highlights only ever live in your database. Skip react-pdf-viewer for new projects: it is archived. The rest of this guide covers the Wingo UI viewer.

## Why build on PDFium in React?

Because PDFium can both read and write a PDF. The viewer loads the PDFium binary (about 4 MB, fetched once and cached) into a web worker, so parsing and painting stay off the main thread, and one worker is shared by every viewer on the page. The same engine that renders the pages also reads the annotations already in the file and writes new ones on export. Running PDFium in React this way lets highlights round-trip through the file. Rendering happens in the browser, and a PDF someone drops onto the viewer opens from memory without an upload.

```bash
npx wingo-ui@latest add pdf-viewer
```

The CLI copies `components/ui/pdf-viewer.tsx`, adds the Button, Popover, Tooltip and Dropdown Menu plus the shared hooks it uses, and installs the `@embedpdf/*` packages, `pdf-lib` (for writing rotations into downloads) and `lucide-react`.

```tsx
import { PdfViewer } from "@/components/ui/pdf-viewer";

export default function ContractPage() {
  return <PdfViewer src="/files/service-agreement.pdf" fileName="service-agreement.pdf" height="80dvh" author="Emma Carter" />;
}
```

`src` takes a same-origin URL, a CORS-enabled URL or a `blob:` URL. With no `src`, the viewer shows an empty state that asks for a file, and `onPdfUpload` hands you the File someone picked or dropped. Pair it with the [File Upload](https://wingo-ui.com/components/file-upload) when the PDF should also go to your server; the [React file upload guide](https://wingo-ui.com/blog/react-file-upload) covers that side.

Scroll the contract, type a page number, open search and try a word like "fee":

> Live demo (PDF Viewer): Open search and type "fee": every hit is marked and the counter steps with Enter. Then drag across a sentence and pick a color in the bar that opens beside it. Try it at [PDF Viewer](https://wingo-ui.com/components/pdf-viewer) and install it with `npx wingo-ui@latest add pdf-viewer`.

## What props does the PDF viewer need?

Only `src` is needed, and `height` is the second one to set. The viewer fills the height you give it (`640` by default, a number for px or any CSS length such as `"100%"` to fill a pane). After that, these are the props you will reach for:

- `defaultZoom`: `"automatic"` (the default) fits the width up to 100%, so a page never opens blown up on a large screen or cut off on a phone. `"fit-page"`, `"fit-width"` or a number also work. `zoomLevels` sets the steps of the zoom menu.
- `variant` (`surface`, `outline`, `plain`) and `radius` for the frame.
- `showToolbar`, `showThumbnails`, `showSearch`, `showRotateControls`, `showDownload`, `showUpload` to trim the chrome.
- `toolbarActions` for your own buttons before the ⋯ menu: Share, Sign, Approve.
- `onActivePageChange` and `onDocumentLoadSuccess(numPages)` to sync a "page 3 of 12" elsewhere in your UI.
- `labels` for every string, merged over `DEFAULT_PDF_VIEWER_LABELS`, and `locale` for the dates in the highlights panel.

`renderPageOverlay` draws on top of every page and rotates with it, for citations or bounding boxes from a document extraction model; `ref.scrollToPageArea(page, { top, left, width, height })` in percent of the page scrolls to one.

## How do I add a React PDF viewer highlight feature?

You already have it: `highlighting` is on by default, and you control the `highlights` array. Select text and a bar opens with five colors, underline, strikethrough, Add note and Copy. Ctrl+Shift+H (⇧⌘H on a Mac) highlights the selection, or toggles the Highlighter, a mode where whatever you select is marked as soon as you let go. Click a highlight to edit its color or note, and Ctrl+Z / ⌘Z undoes.

Each highlight is plain JSON: `{ id, page, rects, text, color, type, note, author, createdAt }`, with `rects` in PDF points from the top-left of the unrotated page. Page coordinates mean a stored highlight lines up at any zoom and rotation. Control the list like any input:

```tsx
"use client";

import { useState } from "react";
import { PdfViewer, type PdfViewerHighlight } from "@/components/ui/pdf-viewer";

export function ContractReview({ documentId, initial }: { documentId: string; initial: PdfViewerHighlight[] }) {
  const [highlights, setHighlights] = useState(initial);

  return (
    <PdfViewer
      src={`/api/documents/${documentId}/file`}
      fileName="service-agreement.pdf"
      author="Emma Carter"
      height="80dvh"
      highlights={highlights}
      onHighlightsChange={(next) => {
        setHighlights(next);
        // the whole list, after every add, edit, delete, undo, redo and import
        void fetch(`/api/documents/${documentId}/highlights`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(next),
        });
      }}
    />
  );
}
```

For one row per highlight, use `onHighlightAdd`, `onHighlightChange` and `onHighlightRemove`. Only `onHighlightsChange` fires for annotations imported from the file, which join the list once per document, merged by id.

> Live demo (PDF Viewer): The Highlighter is on: drag across a clause and it turns yellow as you release. Press Escape or the pen button to turn it off, then click a highlight to add a note. Try it at [PDF Viewer](https://wingo-ui.com/components/pdf-viewer) and install it with `npx wingo-ui@latest add pdf-viewer`.

Set `highlighting={false}` for a read-only review: highlights you pass (or that the file carries) still show and open with their notes, but nothing can change. The Highlights panel lists every highlight with its page, note, author and date, filtered by color; `showHighlightsPanel={false}` hides it when your app shows its own list.

## How do I export PDF annotations from React?

Call `exportPdf()` on the ref. It returns a Blob of the file with every highlight written as a real markup annotation (`/Highlight`, `/Underline` or `/StrikeOut`) with its note and author, plus any page rotations. The export works on a copy. Acrobat, Preview and Chrome show the annotations, and reopening the file in the viewer loads them back as editable highlights. To export PDF annotations without a download button, send that Blob to your server, as below.

```tsx
"use client";

import { useRef } from "react";
import { Button } from "@/components/ui/button";
import { PdfViewer, type PdfViewerHandle } from "@/components/ui/pdf-viewer";

export function SignedCopy({ documentId }: { documentId: string }) {
  const viewer = useRef<PdfViewerHandle>(null);

  const saveAnnotatedCopy = async () => {
    const blob = await viewer.current?.exportPdf();
    if (!blob) return;
    await fetch(`/api/documents/${documentId}/annotated`, {
      method: "PUT",
      headers: { "Content-Type": "application/pdf" },
      body: blob,
    });
  };

  return (
    <PdfViewer
      ref={viewer}
      src={`/api/documents/${documentId}/file`}
      author="Liam Brooks"
      height={600}
      toolbarActions={<Button size="sm" onClick={saveAnnotatedCopy}>Save copy</Button>}
    />
  );
}
```

With `importHighlights` on (the default), markup annotations already in the file become highlights and are replaced by your list on export, so one deleted in the viewer is gone from the file too. Readers get the same export from the ⋯ menu.

## How does the PDF viewer work on a phone?

The layout follows the viewer's own width, so a narrow viewer in a desktop side panel behaves like a phone too. Below 520px the thumbnail sidebar starts closed and slides over the pages. Below 600px the rotate buttons move into the ⋯ menu, and below 440px the Highlights toggle does too. The Highlights panel sits beside the pages from 900px and slides over them below.

On touch, a swipe scrolls. A long press selects the word under your finger, and dragging extends the selection word by word. The color bar pins to the bottom of the viewer (or the top, when the selection is down there), and highlight details open as a bottom sheet. Toolbar and selection bar buttons grow to 44px on coarse pointers. The [mobile-first React components guide](https://wingo-ui.com/blog/mobile-first-react-components) explains these rules.

## Is the PDF viewer accessible?

Yes, it works with a keyboard and a screen reader. The toolbar is a `role="toolbar"` with labeled buttons, the page field takes Enter to jump, Up and Down to step and Escape to cancel, and the search result count is announced through a live region. The thumbnails are a multi-select listbox: Up and Down move, Home and End jump, Shift extends, Space toggles. In the Highlights panel, Up and Down move and Delete removes. Ctrl+F / ⌘F opens search only while focus is inside the viewer, so the browser's own find still works on the rest of the page. Keep in mind that a PDF is only as accessible as its text layer: a scanned file without OCR has no text to search, select or highlight.

## What are the common pitfalls?

- **CORS.** A PDF on another domain needs CORS headers, or the fetch fails and the viewer shows its error state. Proxy it through your own route otherwise.
- **A strict Content Security Policy.** WebAssembly needs `'wasm-unsafe-eval'` in `script-src`. The binary loads from jsDelivr by default; to self-host it, copy `node_modules/@embedpdf/pdfium/dist/pdfium.wasm` into `public/` and pass `wasmUrl="/pdfium.wasm"`.
- **No height.** The viewer does not grow with its pages. Give it `height` or a parent with a fixed height and `height="100%"`.
- **Word files.** PDFium reads PDF only. For .docx, use the [DOCX Viewer](https://wingo-ui.com/components/docx-viewer); the [React DOCX viewer guide](https://wingo-ui.com/blog/react-docx-viewer) covers when to convert a Word file to PDF instead.

## How do I add it to my project?

Run `npx wingo-ui@latest add pdf-viewer`. It is a Pro component ([pricing](https://wingo-ui.com/pricing)), and its [docs page](https://wingo-ui.com/components/pdf-viewer) has more examples, from a read-only review with a Sign button to citations on an invoice. For spreadsheets, the [React Excel viewer guide](https://wingo-ui.com/blog/react-excel-viewer) covers .xlsx files, and the [component guides category](https://wingo-ui.com/blog/category/components) has more single-component deep dives.

## Components in this post

- [PDF Viewer](https://wingo-ui.com/components/pdf-viewer) (Pro): A React PDF viewer on PDFium: sharp pages, search, thumbnails, rotation, and highlights with notes that export as real PDF annotations. Install: `npx wingo-ui@latest add pdf-viewer`
- [DOCX Viewer](https://wingo-ui.com/components/docx-viewer) (Pro): A React Word (.docx) viewer with real pages, headers and footers, thumbnails, zoom, search, comments, tracked changes and download. Install: `npx wingo-ui@latest add docx-viewer`
- [File Upload](https://wingo-ui.com/components/file-upload) (Pro): A React file upload with drag and drop, paste and camera capture: validation, previews, per-file progress, and list, grid or avatar layouts. Install: `npx wingo-ui@latest add file-upload`

## FAQ

### What is the best free React PDF viewer?

For a free, MIT-licensed option, react-pdf by Wojciech Maj renders pages, thumbnails and an outline on top of pdf.js, and you build the toolbar, search and highlights yourself. EmbedPDF is an open-source viewer on a PDFium fork that drops into a page. The Wingo UI PDF Viewer is a Pro component built on the EmbedPDF plugins, with a finished toolbar, a highlights panel and highlights kept as JSON in your state.

### How do I highlight text in a PDF in React?

You need the text positions on the page, a way to turn a selection into rectangles in PDF points, and a layer that draws them at every zoom and rotation. The PDF Viewer does this for you: select text, pick a color in the bar beside the selection, and the highlight lands in the highlights array you pass with onHighlightsChange.

### Can I save highlights into the PDF file itself?

Yes, if they are written as markup annotations. The PDF Viewer's ref.exportPdf() returns a Blob with every highlight as a /Highlight, /Underline or /StrikeOut annotation, with its note and author, so other PDF readers show them and the viewer loads them back when the file is reopened.

### What is the difference between pdf.js and PDFium?

pdf.js is Mozilla's PDF renderer written in JavaScript, licensed Apache 2.0. PDFium is the C++ PDF engine that ships inside Chromium; compiled to WebAssembly, it runs in a web worker in the browser. Both render in the browser, so the file does not have to go to a server.

### Is the Wingo UI PDF Viewer free?

No. The PDF Viewer is a Pro component. The Button, Tooltip, Popover and Dropdown Menu it is built from are free.

---

Source: https://wingo-ui.com/blog/react-pdf-viewer
