•CASE STUDY

PDF Viewer Search Box that Cycles Through Matches (React)

5 min read·854 words·Intermediate

Asked at

1 candidate report in Jul 2026

How to use this case study

SDE-2 / Mid

Design the components (search box, match counter, next/prev buttons, page renderer) and how the search term highlights matches

SDE-3 / Senior

  • Choose state management (context vs Redux/Zustand/MobX)
  • Communicate between the search box and pages (pub/sub)
  • Scroll to matches on pages not yet rendered

Staff / Principal

  • Handle huge PDFs (text index in a web worker, virtualized pages)
  • Performance budgets
  • Accessibility and testing

Problem RestatementProblem

Adobe asked a frontend system design question: in a PDF viewer, build a search box. The user types a word, the viewer highlights all matches in the whole PDF, shows "match 3 of 27", and Next / Previous buttons (and Enter / Shift+Enter) cycle through matches, scrolling to each one, even on pages that aren't rendered yet. The interviewer discussed pub/sub, React context, and state libraries like Redux, Zustand and MobX.

Components

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    SB["SearchBox - input, counter, next/prev"] -->|"setQuery / next / prev"| STORE["Search store"]
    STORE --> VIEW["PdfViewer - virtualized page list"]
    VIEW --> PAGE["Page - canvas + text layer + highlights"]
    WK["Web worker - text index + search"] --> STORE
    STORE --> WK
  • SearchBox: input (debounced), "i of N", next/prev buttons, and keyboard shortcuts.
  • PdfViewer: a virtualized list of pages (only visible pages are rendered).
  • Page: draws the page (canvas) plus a text layer (invisible positioned text spans, like PDF.js), and draws highlight boxes for matches on that page.
  • Search store: { query, matches: [{page, start, end}], activeIndex }.

Searching

  • At load time, extract text per page (PDF.js getTextContent) in a Web Worker, so the UI thread isn't blocked. Build a per-page text string plus a mapping from character offsets to text spans (for highlight positions).
  • On query (debounced ~200 ms): the worker finds all matches (case-insensitive, normalized whitespace) across pages and returns an ordered list (page, charStart, charEnd). Cancel an old search if the user keeps typing.
  • For large PDFs, stream results back page by page, so the counter grows ("27+ matches...").

State and Communication

  • Where to keep state: a small store shared by the search box and the pages. Options:
  • React Context + useReducer: fine, but any change re-renders every consumer. Avoid it for fast-changing state if many pages subscribe.
  • Zustand / Redux with selectors (our choice): each Page subscribes only to its own matches and whether the active match is on it (select(s => s.matchesByPage[pageNo])), so moving to the next match re-renders just 1–2 pages.
  • MobX: observable state with fine-grained updates, which also works well.
  • Pub/sub view: the store acts as the event bus. The search box publishes next(), and pages and the viewer react to activeIndex changes.

Deep Dive — Jumping to a match on a page that is not renderedDeep dive

A 900-page PDF, virtualised so only the visible pages exist in the DOM. The user searches a word and presses Next to cycle through matches.

Weak

Render every page so matches exist in the DOM

Drop the virtualisation and mount all pages, then use ordinary DOM search and scrolling.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  ALL["Render all 900 pages"] --> DOM["Hundreds of thousands of text nodes"]
  DOM --> MEM["Memory balloons - the tab becomes unresponsive"]
  ALL --> TIME["Initial load takes many seconds"]
  MEM --> CRASH["Large documents crash the tab"]

It makes searching easy by making the viewer unusable. Virtualisation exists precisely because the document does not fit, so removing it trades the hard problem for a worse one.

Good

Search only the rendered pages

Keep virtualisation and search what is currently mounted.

The viewer stays fast and the feature is wrong: the match count reflects three pages rather than the document, and Next can only reach matches that happen to be on screen. The user's mental model — "find every occurrence" — is not satisfied.

Best

Search the text layer independently of rendering

The text and page geometry come from the PDF itself, so searching does not require a rendered page:

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  PDF["PDF text layer + page metadata"] --> IDX["Search all pages - off the main thread"]
  IDX --> LIST["Ordered match list: page, rect, index"]
  NEXT["next()"] --> AI["activeIndex = (activeIndex + 1) % N - wraps"]
  AI --> PG["Look up the match's page"]
  PG --> OFF["Compute scroll offset from known page heights - no render needed"]
  OFF --> SCROLL["Scroll the virtualised list"]
  SCROLL --> MOUNT["Page mounts, renders its text layer and highlights"]
  MOUNT --> CENTER["Scroll the exact match rectangle into view"]
  CENTER --> HL["Active match highlighted strongly, others lightly"]
  • Matches are found in the document, not the DOM, so the count is the true total and Next can reach any of them. Run it off the main thread for large documents so typing stays responsive.
  • Page heights come from the PDF metadata, which is what makes the scroll offset computable without rendering anything — the key that unlocks the whole approach.
  • Two-stage scroll. Scroll the list to the page, then, once it mounts, centre the exact match rectangle. Trying to do it in one step fails because the rectangle does not exist yet.
  • Distinguish the active match with a stronger highlight, or cycling through twelve identical highlights tells the user nothing about where they are.

Debounce the query and search incrementally as the user types — a full-document search per keystroke is the other way this feature becomes slow, even when nothing is being rendered.

Performance and AccessibilityScale

  • Only visible pages (plus a buffer) have canvases. Highlights are simple absolutely positioned divs over the text layer.
  • The search runs in a worker, and the result list is compact. Memoize per-page highlight rectangles.
  • Accessibility: aria-live announcements ("Match 3 of 27"), keyboard shortcuts (Ctrl+F focuses the box, Enter/Shift+Enter navigates, Esc clears), and visible focus.
  • Tests: unit tests for the store logic (wrap-around, empty results), and integration tests that navigate to a match on page 80.

Wrap-UpWrap-up

Extract page text in a web worker and search it there, producing an ordered list of (page, start, end) matches. Keep query, matches and the active index in a selector-based store (Zustand/Redux or MobX), so only affected pages re-render. Cycle with wrap-around, scroll the virtualized viewer to the target page using known page heights, then scroll the exact highlight into view once the page's text layer renders, with debounced input, keyboard shortcuts and live announcements.

More Case Studies

Frequently Asked Questions

What is the PDF Viewer Search Box that Cycles Through Matches (React) system design question?

PDF Viewer Search Box that Cycles Through Matches (React) is a system design interview question asked at FAANG companies. It covers frontend, algorithms, concurrency and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the PDF Viewer Search Box that Cycles Through Matches (React) question?

Adobe have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the PDF Viewer Search Box that Cycles Through Matches (React) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the PDF Viewer Search Box that Cycles Through Matches (React) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →