---
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-06-12T22:16:41.708Z"
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)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.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)
- [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)
- [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)
- [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)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Process Flows](/blog/process-flows.md)
- [or](/blog/sample-blog-updated.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)

