---
title: "How to implement text search in PDF.js with PDFFindController"
canonical_url: "https://www.nutrient.io/blog/pdfjs-text-search-pdffindcontroller/"
md_url: "https://www.nutrient.io/blog/pdfjs-text-search-pdffindcontroller.md"
last_updated: "2026-08-11T08:57:05.236Z"
description: "Learn how to wire up PDF.js’s built-in PDFFindController for full-text search, including a complete React search toolbar component with match navigation and search options."
---

**TL;DR**

PDF.js’s `PDFFindController` handles full-document text search. You control it entirely through `EventBus` events — never direct method calls. The wiring has three parts:

- **Setup** — Construct `PDFFindController({ linkService, eventBus })` and call `findController.setDocument(pdfDocument)` after the document loads.

- **Dispatch** — Call `eventBus.dispatch("find", { type, query, caseSensitive, entireWord, highlightAll, matchDiacritics, findPrevious })` to trigger or update a search. `type: ""` starts a new search; `type: "again"` walks to the next or previous match.

- **Listen** — Subscribe to `updatefindmatchescount` for running totals and `updatefindcontrolstate` for the final `FindState` (`FOUND`, `NOT_FOUND`, `PENDING`).

The controller scrolls to and highlights the active match for you. If you’d rather skip the `EventBus` dance, [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) exposes `instance.search()` and a built-in search UI.

PDF.js includes a built-in `PDFFindController` that handles text search across all pages. This guide shows how to wire it up with a custom React search UI, handle match counts, and navigate between results.

## Setup

The `PDFFindController` is created during viewer initialization and needs both the `EventBus` and `PDFLinkService`:

```tsx

const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");

const eventBus = new pdfjs.EventBus();
const linkService = new pdfjs.PDFLinkService({ eventBus });
const findController = new pdfjs.PDFFindController({ linkService, eventBus });

// After document loads:
findController.setDocument(pdfDocument);

```

## Triggering a search

Search is controlled entirely through `EventBus` `find` events. You never call methods on the find controller directly — you dispatch events:

```tsx

eventBus.dispatch("find", {
  type: "",              // "" for new search, "again" for next/prev
  query: "neural network",
  caseSensitive: false,
  entireWord: false,
  highlightAll: true,
  matchDiacritics: false,
  findPrevious: false,   // true = search backwards
});

```

### The type parameter

| Value                       | Purpose                                   |
| --------------------------- | ----------------------------------------- |
| `""`                        | Start a new search                        |
| `"again"`                   | Navigate to next/previous result          |
| `"casesensitivitychange"`   | Re-search with changed case sensitivity   |
| `"entirewordchange"`        | Re-search with changed whole-word setting |
| `"highlightallchange"`      | Toggle highlight-all                      |
| `"diacriticmatchingchange"` | Toggle diacritics matching                |

## Listening for results

Subscribe to two events to track search state:

```tsx

// Match count updates.
eventBus.on("updatefindmatchescount", (evt) => {
  console.log(`Result ${evt.matchesCount.current} of ${evt.matchesCount.total}`);
});

// Find state changes.
eventBus.on("updatefindcontrolstate", (evt) => {
  // evt.state is a FindState enum value
  // evt.matchesCount = { current, total }
  switch (evt.state) {
    case pdfjs.FindState.FOUND:
      // Match found.
      break;
    case pdfjs.FindState.NOT_FOUND:
      // No matches.
      break;
    case pdfjs.FindState.PENDING:
      // Still searching...
      break;
  }
});

```

## Complete React search component

```tsx

import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { FindState } from "pdfjs-dist/web/pdf_viewer.mjs";
import { PDFContext } from "./PDFContext";

function SearchToolbar() {
  const { eventBus } = useContext(PDFContext);
  const [query, setQuery] = useState("");
  const [matchCount, setMatchCount] = useState({ current: 0, total: 0 });
  const [findState, setFindState] = useState(null);

  // Search options.
  const [caseSensitive, setCaseSensitive] = useState(false);
  const [entireWord, setEntireWord] = useState(false);
  const [highlightAll, setHighlightAll] = useState(false);
  const [matchDiacritics, setMatchDiacritics] = useState(false);

  // Track previous values to determine which option changed.
  const prevQuery = useRef("");
  const prevCaseSensitive = useRef(false);
  const prevEntireWord = useRef(false);
  const prevHighlightAll = useRef(false);
  const prevMatchDiacritics = useRef(false);

  const dispatchFind = useCallback(
    (type, findPrevious = false) => {
      eventBus.current?.dispatch("find", {
        type,
        query,
        caseSensitive,
        entireWord,
        highlightAll,
        matchDiacritics,
        findPrevious,
      });
    },
    [eventBus, query, caseSensitive, entireWord, highlightAll, matchDiacritics],
  );

  // Dispatch appropriate event when any search parameter changes.
  useEffect(() => {
    if (query!== prevQuery.current) {
      dispatchFind("");
    } else if (caseSensitive!== prevCaseSensitive.current) {
      dispatchFind("casesensitivitychange");
    } else if (entireWord!== prevEntireWord.current) {
      dispatchFind("entirewordchange");
    } else if (highlightAll!== prevHighlightAll.current) {
      dispatchFind("highlightallchange");
    } else if (matchDiacritics!== prevMatchDiacritics.current) {
      dispatchFind("diacriticmatchingchange");
    }

    prevQuery.current = query;
    prevCaseSensitive.current = caseSensitive;
    prevEntireWord.current = entireWord;
    prevHighlightAll.current = highlightAll;
    prevMatchDiacritics.current = matchDiacritics;
  }, [query, caseSensitive, entireWord, highlightAll, matchDiacritics, dispatchFind]);

  // Listen for results.
  useEffect(() => {
    const onMatchCount = (evt) => setMatchCount(evt.matchesCount);
    const onState = (evt) => {
      setFindState(evt.state);
      setMatchCount(evt.matchesCount);
    };

    eventBus.current?.on("updatefindmatchescount", onMatchCount);
    eventBus.current?.on("updatefindcontrolstate", onState);

    return () => {
      eventBus.current?.off("updatefindmatchescount", onMatchCount);
      eventBus.current?.off("updatefindcontrolstate", onState);
    };
  }, [eventBus]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <span>
        {findState === FindState.NOT_FOUND? "No results"
          : `${matchCount.current} of ${matchCount.total}`}
      </span>
      <button onClick={() => dispatchFind("again", true)}>Previous</button>
      <button onClick={() => dispatchFind("again", false)}>Next</button>
      <label>
        <input
          type="checkbox"
          checked={caseSensitive}
          onChange={() => setCaseSensitive(!caseSensitive)}
        />
        Case sensitive
      </label>
      <label>
        <input
          type="checkbox"
          checked={entireWord}
          onChange={() => setEntireWord(!entireWord)}
        />
        Whole word
      </label>
      <label>
        <input
          type="checkbox"
          checked={highlightAll}
          onChange={() => setHighlightAll(!highlightAll)}
        />
        Highlight all
      </label>
    </div>
  );
}

```

## How it works under the hood

1. You dispatch a `find` event on the `EventBus`

2. `PDFFindController` receives it and searches the text layer of each page

3. It dispatches `updatefindmatchescount` with running totals

4. It dispatches `updatefindcontrolstate` with the final state

5. It automatically scrolls to and highlights the current match

6. Built-in CSS classes (`.highlight`, `.highlight.selected`) style the matches

## Key points

- All search interaction goes through `EventBus` events, never direct method calls

- Use `type: ""` for new searches, and `type: "again"` for next/previous navigation

- The `findPrevious` Boolean controls search direction when using `type: "again"`

- `highlightAll: true` highlights all matches on visible pages, not just the current one

- Track previous option values to dispatch the correct change event type

- The find controller handles page-by-page searching automatically — no manual page iteration needed

## FAQ

#### Why does <code>PDFFindController</code> use events instead of method calls?

PDF.js was designed around its viewer layer, which is fully event-driven. The find controller listens for `find` events on the `EventBus`, so the same code path serves the built-in PDF.js toolbar, your custom React UI, and any other client. Calling methods directly would skip the controller’s internal queuing and result tracking, which is why PDF.js doesn’t expose them.

#### What’s the difference between <code>type: ""</code> and <code>type: "again"</code>?

`type: ""` starts a fresh search — the controller rebuilds its internal match list from the current `query` and options. `type: "again"` navigates within the existing match list using the `findPrevious` flag (`false` for next, `true` for previous). If you dispatch `"again"` without first running a `""` search, it’s a no-op.

#### Why are there separate <code>*change</code> event types for each option?

PDF.js needs to know which option changed so it can decide whether to rerun the full search or just refilter existing matches. Toggling `highlightAll` only redraws — no re-search needed — while changing `caseSensitive` invalidates the match list and forces a rerun. The granular `type` values let the controller pick the cheapest path.

#### Why do I get two events (<code>updatefindmatchescount</code> and <code>updatefindcontrolstate</code>)?

The controller searches asynchronously. `updatefindmatchescount` fires repeatedly as new pages finish indexing, so you can show a running total. `updatefindcontrolstate` fires once with the final `FindState` (`FOUND`, `NOT_FOUND`, `PENDING`, or `WRAPPED`) and the definitive match count. Most UIs listen to both — totals to the count event, state to the control event.

#### How do I search across the entire document, not just visible pages?

That’s the default. `PDFFindController` walks every page’s text content, not just the rendered ones. You don’t need to scroll the viewer or preload pages. The catch: If you want match counts to update incrementally, leave `highlightAll: true` so PDF.js paints found matches as soon as a page is indexed.

#### How does Nutrient Web SDK compare for search?

Nutrient exposes `instance.search(query)` returning an array of matches with page indexes and bounding rectangles. Combined with `instance.setSearchState()`, you can drive the built-in search UI directly without managing `EventBus` state. The SDK also indexes annotations and form fields, so searches return hits that PDF.js’s text-layer search would miss.

## How Nutrient Web SDK handles this

Two lines replace the entire `PDFFindController` setup, `EventBus` dispatch, and result listener wiring shown above:

```js

// Search across all pages.
const results = await instance.search("neural network");

// Display the results in the built-in search UI.
instance.setSearchState((state) => state.set("results", results));

```

The built-in search UI also provides match counts, case sensitivity, and result navigation.

---

_See [Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) for a built-in search API and UI, 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)
- [`vector_store` holds your indexed documents (see the multimodal RAG post](/blog/agentic-rag.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Ai Legal Assistant Document Authoring](/blog/ai-legal-assistant-document-authoring.md)
- [Angular File Viewer Pdf Image Office Files](/blog/angular-file-viewer-pdf-image-office-files.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Best Document Ai Platforms](/blog/best-document-ai-platforms.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)
- [1. Extract and chunk the PDF.](/blog/chat-with-pdf.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Construction Document Data Extraction](/blog/construction-document-data-extraction.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Create And Edit Pdfs In Flutter](/blog/create-and-edit-pdfs-in-flutter.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [Creating And Filling Pdf Forms Programmatically In Javascript](/blog/creating-and-filling-pdf-forms-programmatically-in-javascript.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Extraction Confidence Scores](/blog/document-extraction-confidence-scores.md)
- [Document Viewer](/blog/document-viewer.md)
- [Document Watermarking](/blog/document-watermarking.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [Extract Patient Data On Premises](/blog/extract-patient-data-on-premises.md)
- [app.py](/blog/extract-text-from-pdf-using-python.md)
- [Fillable Pdf](/blog/fillable-pdf.md)
- [How To Add Digital Signature To Pdf Using React](/blog/how-to-add-digital-signature-to-pdf-using-react.md)
- [How To Build A Dotnet Maui Pdf Viewer](/blog/how-to-build-a-dotnet-maui-pdf-viewer.md)
- [How To Build A Flutter Pdf Viewer](/blog/how-to-build-a-flutter-pdf-viewer.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [How To Build A Javascript Pdf Viewer](/blog/how-to-build-a-javascript-pdf-viewer.md)
- [or](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [How To Build A Powerpoint Viewer Using Javascript](/blog/how-to-build-a-powerpoint-viewer-using-javascript.md)
- [Using Yarn](/blog/how-to-build-a-react-excel-viewer.md)
- [How To Build A React Native Pdf Viewer](/blog/how-to-build-a-react-native-pdf-viewer.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [How To Build A Reactjs File Viewer](/blog/how-to-build-a-reactjs-file-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer With Pdfjs](/blog/how-to-build-a-vuejs-pdf-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer](/blog/how-to-build-a-vuejs-pdf-viewer.md)
- [How To Build An Android Pdf Viewer](/blog/how-to-build-an-android-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Ng2 Pdf Viewer](/blog/how-to-build-an-angular-pdf-viewer-with-ng2-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Pdfjs](/blog/how-to-build-an-angular-pdf-viewer-with-pdfjs.md)
- [How To Convert Docx To Pdf Using Javascript](/blog/how-to-convert-docx-to-pdf-using-javascript.md)
- [How To Convert Docx To Pdf Using Python](/blog/how-to-convert-docx-to-pdf-using-python.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [or](/blog/how-to-convert-html-to-pdf-using-react.md)
- [How To Convert Html To Pdf Using Wkhtmltopdf And Csharp](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-csharp.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [or](/blog/how-to-create-a-react-js-signature-pad.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How To Edit Pdfs Using Ios Pdf Library](/blog/how-to-edit-pdfs-using-ios-pdf-library.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [How To Extract Tables From Pdf And Images](/blog/how-to-extract-tables-from-pdf-and-images.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)
- [How To Merge Pdfs Using Javascript](/blog/how-to-merge-pdfs-using-javascript.md)
- [How To Ocr Pdfs In Linux](/blog/how-to-ocr-pdfs-in-linux.md)
- [How To Print Pdf In Csharp](/blog/how-to-print-pdf-in-csharp.md)
- [Open an image.](/blog/how-to-use-tesseract-ocr-in-python.md)
- [From an HTML string.](/blog/html-in-pdf-format.md)
- [Javascript Pdf Editors](/blog/javascript-pdf-editors.md)
- [Javascript Pdf Libraries](/blog/javascript-pdf-libraries.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [or](/blog/merge-pdfs.md)
- [Swift Package Manager](/blog/mobile-pdf-sdk.md)
- [`elements` come from your document parser — each has a type and content.](/blog/multimodal-rag.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)
- [Building WCAG 2.2, Section 508, and PDF/UA-compliant PDFs with an SDK](/blog/pdf-accessibility.md)
- [Pdf Data Extraction Developer Guide](/blog/pdf-data-extraction-developer-guide.md)
- [Pdf Extraction Benchmark Opendataloader Bench](/blog/pdf-extraction-benchmark-opendataloader-bench.md)
- [Pdf Extraction Document Case Studies](/blog/pdf-extraction-document-case-studies.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdfjs Accessibility Structtree Printing](/blog/pdfjs-accessibility-structtree-printing.md)
- [Pdfjs Advanced Loading Streaming Workers](/blog/pdfjs-advanced-loading-streaming-workers.md)
- [Pdfjs Annotation Editor Layer](/blog/pdfjs-annotation-editor-layer.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 Document Outline Bookmarks Metadata](/blog/pdfjs-document-outline-bookmarks-metadata.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [macOS](/blog/pdfjs-file-format-conversion-to-pdf.md)
- [macOS](/blog/pdfjs-generating-pdf-thumbnails-pdf2pic.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Native Annotation Layer Forms](/blog/pdfjs-native-annotation-layer-forms.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs Pdf Page Manipulation Pdf Lib](/blog/pdfjs-pdf-page-manipulation-pdf-lib.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Pdfjs Server Side Text Extraction](/blog/pdfjs-server-side-text-extraction.md)
- [Pdfjs Sticky Note Annotations](/blog/pdfjs-sticky-note-annotations.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Thumbnail Sidebar](/blog/pdfjs-thumbnail-sidebar.md)
- [Process Flows](/blog/process-flows.md)
- [React Native Pdf Annotation](/blog/react-native-pdf-annotation.md)
- [Using Yarn](/blog/react-pdf-editor.md)
- [or](/blog/sample-blog-updated.md)
- [Sdk Product Updates Q2 2026](/blog/sdk-product-updates-q2-2026.md)
- [Add DWS MCP Server to your Claude Code project.](/blog/teaching-llms-to-read-pdfs.md)
- [Open an image file.](/blog/tesseract-python-guide.md)
- [Define the HTML part of the document.](/blog/top-10-ways-to-generate-pdfs-in-python.md)
- [Top 5 Javascript Pdf Viewers](/blog/top-5-javascript-pdf-viewers.md)
- [or](/blog/top-js-pdf-libraries.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Is Document Processing](/blog/what-is-document-processing.md)
- [What Is Intelligent Document Processing](/blog/what-is-intelligent-document-processing.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)

