EmbedPDF

Search

The search plugin finds text in the whole document and highlights every match. Register searchPlugin(), put <SearchLayer> in each page, and search from your own search box:

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { SearchLayer, searchPlugin } from '@embedpdf/svelte/search';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import SearchBox from './SearchBox.svelte';

  import '../basic.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), searchPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <SearchBox />
    <Stage class="stage">
      <RenderLayer />
      <SearchLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Try it: type a word, then press Enter for the next match and Shift+Enter for the one before. Each move scrolls the match into view.

search-box.svelte
<script lang="ts">
  import { RenderLayer } from '@embedpdf/svelte/render';
  import { SearchLayer, useSearch, useSearchState } from '@embedpdf/svelte/search';
  import { Stage } from '@embedpdf/svelte/stage';

  const search = useSearch();
  const state = useSearchState();
</script>

<form onsubmit={(event) => event.preventDefault()}>
  <input oninput={(event) => search.search({ text: event.currentTarget.value })} />
  {#if state.status === 'searching'}
    <span>Searching…</span>
  {/if}
  {#if state.hitCount > 0}
    <span>{state.activeHitIndex + 1} of {state.hitCount}</span>
  {/if}
  <button type="button" onclick={() => search.previousHit()}>↑</button>
  <button type="button" onclick={() => search.nextHit()}>↓</button>
</form>

<Stage>
  <RenderLayer />
  <SearchLayer />
</Stage>
  • Matches appear as they’re found. Large documents are searched a few pages at a time, and the count and the highlights grow as the search goes.
  • Searching again replaces the search. Type another letter and the old search stops; an empty text clears it.
  • The first match becomes the active one without moving the view. nextHit(), previousHit() and goToHit(hit) move to a match and bring it into view, and wrap around at the ends.

Search options#

search.search({ text: 'invoice total', wholeWord: true, matchCase: false });

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { SearchLayer, searchPlugin } from '@embedpdf/svelte/search';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import SearchWithOptions from './SearchWithOptions.svelte';

  import '../options.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), searchPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <SearchWithOptions />
    <Stage class="stage">
      <RenderLayer />
      <SearchLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Try it: turn on Match case and pdf no longer finds PDF; turn on Whole words and it no longer finds EmbedPDF.

OptionWhat it doesDefault
textWhat to findrequired
matchCaseInvoice doesn’t match invoicefalse
wholeWordcat doesn’t match categoryfalse
matchDiacriticsresume doesn’t match résuméfalse
ignoreWhitespaceinvoice also finds i n v o i c e, for text with odd spacingfalse
regextext is a regular expressionfalse

A search starts from the page the reader is on, and { from: page.ref } starts it elsewhere.

A list of results#

useSearchHits() gives you every match found so far, each with a few words around it:

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { SearchLayer, searchPlugin } from '@embedpdf/svelte/search';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import Results from './Results.svelte';
  import SearchBox from './SearchBox.svelte';

  import '../results-list.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), searchPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <SearchBox />
    <div class="viewer">
      <Stage class="stage">
        <RenderLayer />
        <SearchLayer />
      </Stage>
      <Results />
    </div>
  </DocumentGate>
</Viewer>
results.svelte
<script lang="ts">
  import { useSearch, useSearchHits } from '@embedpdf/svelte/search';

  const search = useSearch();
  const hits = useSearchHits();
</script>

<ol>
  {#each hits.current as hit, index (index)}
    <li>
      <button type="button" onclick={() => search.goToHit(hit)}>
        Page {hit.pageIndex + 1}: …{hit.snippet?.before}
        <mark>{hit.snippet?.match}</mark>
        {hit.snippet?.after}…
      </button>
    </li>
  {/each}
</ol>

useSearchHits(page.ref) gives the matches on one page, for a count per thumbnail.

FieldWhat it is
pageThe page it’s on
pageIndexThat page’s position, from 0
start, countThe characters it covers: a range you can select or highlight
boundsA box around it, in page coordinates
segmentsOne piece per line it covers, each with its four corners
snippetThe words before, the match and the words after

Highlight or select a match#

A match is a range of characters, the same kind a selection and a highlight use, so it goes straight into either:

const hit = search.getActiveHit();
 
selection.select(hit); // select it
await annotation.create(hit.page, {
  subtype: 'highlight',
  quadPoints: hit.segments.map((segment) => segment.quad),
}); // highlight it

Search without showing it#

findAll() searches the whole document and gives you the matches, without touching the search box’s results or the highlights:

const hits = await search.findAll({ text: 'confidential', wholeWord: true });

Use it for a “mark every occurrence” feature, or to redact every match.

Change how matches look#

Set the highlight colors when you register the plugin:

searchPlugin({
  highlight: { color: '#fde047', activeColor: '#f97316' },
});

On dark or scanned pages, where a multiplied highlight disappears, use translucent colors and blendMode: 'normal'. Each of these can also come from CSS, which wins over the setting, for example in dark mode:

@media (prefers-color-scheme: dark) {
  .pdf-viewer {
    --epdf-search-highlight: rgb(253 224 71 / 0.4);
    --epdf-search-blend-mode: normal;
  }
}

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { SearchLayer, searchPlugin } from '@embedpdf/svelte/search';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import ColorPicker from './ColorPicker.svelte';

  import '../highlight-colors.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    searchPlugin({ highlight: { color: '#ffd500', activeColor: '#ff9632' } }),
  ];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<script lang="ts">
  let fromCss = $state(false);
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <div class="toolbar">
      <ColorPicker />
      <label class="option">
        <input type="checkbox" bind:checked={fromCss} />
        Override with CSS
      </label>
    </div>
    <Stage class={fromCss ? 'stage from-css' : 'stage'}>
      <RenderLayer />
      <SearchLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Try it: pick a color, and search.updateSettings({ highlight }) changes it at once. Then turn on Override with CSS: the CSS variables win over the color you picked, and their translucent highlights, drawn with blendMode: 'normal', stay visible on the dark cover.

Click a match on the page#

Give <SearchLayer onHitClick> a function, and it’s called with the match someone clicks. goToHit() takes that match as it is, so making it the active one is one line:

hit-click.svelte
<script lang="ts">
  import { SearchLayer, useSearch } from '@embedpdf/svelte/search';

  const search = useSearch();
</script>

<SearchLayer onHitClick={(hit) => search.goToHit(hit)} />

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { searchPlugin } from '@embedpdf/svelte/search';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import ActiveMatch from './ActiveMatch.svelte';
  import Pages from './Pages.svelte';

  import '../click-match.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), searchPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ActiveMatch />
    <Pages />
  </DocumentGate>
</Viewer>

The text under a match stays selectable: a drag that starts on a match selects text, and only a click calls the function. Without one, matches are only paint, and the pointer goes straight through to the page.

Methods#

Everything useSearch() gives you.

MethodWhat it does
search(query, { from? })Starts a search, replacing the one before, and highlights the matches as they’re found. Resolves { status, hitCount } when it’s done, replaced or cancelled. An empty text clears. Search options
nextHit(reveal?), previousHit(reveal?)Makes the next or previous match the active one and scrolls to it, wrapping at the ends. Returns the match, or null when there is none.
goToHit(hit, reveal?)Makes one match the active one and scrolls to it: the match itself, or its place in the list. Click a match on the page
revealActiveHit(reveal?)Scrolls back to the active match without changing it
listHits({ page? })Every match found so far, or the ones on one page. In a component, use useSearchHits().
listPagesWithHits()The pages that have a match, in document order
cancel()Stops searching and keeps the matches found so far
clear()Stops searching and removes the matches and their highlights
refresh()Runs the current search again from the start. The plugin does this by itself when the document changes.
findAll(query, { snippets? })Finds every match without showing them or touching the current search. snippets: false leaves out the words around each match. Search without showing it
canSearch({ snippets? })Whether searching is allowed; with snippets: true, whether matches carry the words around them. Permissions
getSettings(), updateSettings(changes), resetSettings()Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use useSearchSettings(). Settings

reveal is { anchor, behavior }, the reveal setting for one call. Every method that returns a promise also takes a signal in its options, to cancel it.

State#

Everything useSearchState() gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.

FieldWhat it isRead once
queryThe last search, with its options, or nullgetQuery()
status'idle', 'searching', 'complete', 'cancelled' or 'error'getStatus()
hitCountHow many matches were found so fargetHitCount(), or getHitCount(page) for one page
activeHitIndexThe active match’s place in the list, from 0, or -1getActiveHitIndex()
activeHitThe active match, or nullgetActiveHit()
progressHow far the search got: pagesSearched of pageCountgetProgress()
errorWhy the search failed, when status is 'error'getError()

The matches themselves come from useSearchHits().

Settings#

Set them when you register the plugin, and change them while the app runs with search.updateSettings({ … }); search.resetSettings() goes back to what you registered, and useSearchSettings() gives you the current settings.

SettingWhat it doesDefaultFrom CSS
reveal.anchorWhere a match lands in the view when you move to it{ y: 0.35 }, like a browser—
reveal.behaviorGlide there ('smooth') or jump ('instant')'smooth'—
highlight.colorThe highlight of every match'#ffd500'--epdf-search-highlight
highlight.activeColorThe highlight of the active match'#ff9632'--epdf-search-highlight-active
highlight.blendModeHow the highlights mix with the page'multiply'--epdf-search-blend-mode

Events#

match-count.svelte
<script lang="ts">
  import { useSearchEvent } from '@embedpdf/svelte/search';

  let message = $state('');

  useSearchEvent(
    (search) => search.onCompleted,
    ({ hitCount }) => (message = `${hitCount} matches`),
  );
</script>

<p aria-live="polite">{message}</p>
EventFires whenCarries
onStartedA search startedquery
onProgressChangedThe search’s progress changed: more pages were searched, or a new search started overhitCount, pagesSearched, pageCount
onCompletedThe whole document was searchedhitCount
onCancelledA search stopped before the endreason
onFailedA search failederror
onActiveHitChangedAnother match became the active oneindex, hit
onClearedThe search was cleared—
onSettingsChangedThe settings changedsettings, changed

Permissions#

PermissionWhat it allowsCheck
doc.text.searchSearchingsearch.canSearch()
doc.text.copyThe words around each match (snippet)search.canSearch({ snippets: true })

Without doc.text.copy, search still finds and highlights every match, but a match has no snippet.

Good to know#

Searches stay up to date#

When the document changes, for example when pages are inserted or text is redacted, the current search runs again, so the matches and the count stay right.

Searching large documents#

The local engine searches the file in the browser. Big documents are searched a few pages at a time, so the page stays responsive and results arrive as they’re found.

Was this page helpful?

Your feedback goes directly to the documentation team.