Skip to content

Add a Search Function and Show Search Results in the Sidebar


Scenario

When viewing a PDF document in a Vue PDF component, users often need to search for specific text and quickly jump to each matched result. In this example, we build a custom search sidebar so users can:

  • Enter a keyword
  • View all matches grouped by page
  • Click a result to navigate to that exact match in the document
  • View highlight(s) of the currently selected match in the document

What to Use

Use the searchControl from Instance API to handle searching and match navigation in Vue PDF Viewer.

The Search Controller gives you access to the search state, results, and navigation functions to programmatically control the search experience. Here are the key functions for this example:

NameObjective
searchTrigger a text search when users submit a keyword
searchingTrack loading state while searching
searchMatchesRead matched results that are grouped by page
goToMatchJump to a selected match when users click on a result

Code example

vue
<!-- Script -->
<script setup lang="ts">
import { computed, nextTick, ref } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";

// Type of the search match object
type SearchMatch = {
  pageNumber: number;
  matchNumber: number;
};

// Type of the search control object
type SearchControl = {
  goToMatch: (matchNumber: number) => void;
  search: (value: string) => void;
  searching?: boolean;
  searchMatches?: {
    totalMatches: number;
    matches: Array<{ index: number; page: number }>;
  };
};

const toolbarOptions = { searchable: false } as const;
const pdfViewerRef = ref<InstanceType<typeof VPdfViewer>>();
const searchInputRef = ref<HTMLInputElement | null>(null);
const searchKeyword = ref("");
const isSearchSidebarOpen = ref(false);

// Return the search control object
const searchControl = computed(
  () => pdfViewerRef.value?.searchControl as SearchControl | undefined
);
// Return the searching state
const isSearching = computed(() => searchControl.value?.searching ?? false);

// Return the search results
const searchResults = computed(() => {
  const searchMatchesSource = searchControl.value?.searchMatches;
  const rawMatches = searchMatchesSource?.matches ?? [];
  const matches = rawMatches.map((match, index) => ({
    pageNumber: match.page,
    matchNumber: index + 1, // goToMatch expects a 1-based match index
  }));

  return {
    totalMatches: searchMatchesSource?.totalMatches ?? 0,
    matches,
  };
});

// Return the matches grouped by page
const matchesGroupedByPage = computed(() => {
  const pageGroups = new Map<number, SearchMatch[]>();

  // Group by page, then sort page sections in ascending order
  for (const match of searchResults.value.matches) {
    const pageMatches = pageGroups.get(match.pageNumber) ?? [];
    pageMatches.push(match);
    pageGroups.set(match.pageNumber, pageMatches);
  }

  return [...pageGroups.entries()]
    .sort(([leftPage], [rightPage]) => leftPage - rightPage)
    .map(([pageNumber, matches]) => ({ pageNumber, matches }));
});

const submitSearch = () => {
  searchControl.value?.search(searchKeyword.value.trim());
};

const openMatch = (matchNumber: number) => {
  searchControl.value?.goToMatch(matchNumber);
};

const toggleSearchSidebar = () => {
  isSearchSidebarOpen.value = !isSearchSidebarOpen.value;

  // Focus the search input if the search sidebar is opened
  if (isSearchSidebarOpen.value) {
    void nextTick(() => {
      searchInputRef.value?.focus();
    });
  }
};
</script>

<!-- Template -->
<template>
  <div id="vpv">
    <div class="pdf-viewer-wrapper">
      <!-- PDF Viewer -->
      <VPdfViewer
        ref="pdfViewerRef"
        src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
        :toolbar-options="toolbarOptions"
      />

      <!-- Search Toggle Button -->
      <button
        type="button"
        class="pdf-search-toggle-btn"
        :class="{ 'is-active': isSearchSidebarOpen }"
        title="Toggle search panel"
        @click="toggleSearchSidebar"
      >
        <svg
          width="20px"
          height="20px"
          viewBox="0 0 20 20"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M17.9419 17.0577L14.0302 13.1468C15.1639 11.7856 15.7293 10.0398 15.6086 8.27238C15.488 6.50499 14.6906 4.85217 13.3823 3.65772C12.074 2.46328 10.3557 1.8192 8.58462 1.85944C6.81357 1.89969 5.12622 2.62118 3.87358 3.87383C2.62094 5.12647 1.89945 6.81382 1.8592 8.58486C1.81895 10.3559 2.46304 12.0743 3.65748 13.3825C4.85192 14.6908 6.50475 15.4882 8.27214 15.6089C10.0395 15.7295 11.7854 15.1642 13.1466 14.0304L17.0575 17.9421C17.1156 18.0002 17.1845 18.0463 17.2604 18.0777C17.3363 18.1091 17.4176 18.1253 17.4997 18.1253C17.5818 18.1253 17.6631 18.1091 17.739 18.0777C17.8149 18.0463 17.8838 18.0002 17.9419 17.9421C17.9999 17.8841 18.046 17.8151 18.0774 17.7392C18.1089 17.6634 18.125 17.5821 18.125 17.4999C18.125 17.4178 18.1089 17.3365 18.0774 17.2606C18.046 17.1848 17.9999 17.1158 17.9419 17.0577ZM3.12469 8.74993C3.12469 7.63741 3.45459 6.54988 4.07267 5.62485C4.69076 4.69982 5.56926 3.97885 6.5971 3.55311C7.62493 3.12737 8.75593 3.01598 9.84707 3.23302C10.9382 3.45006 11.9405 3.98579 12.7272 4.77246C13.5138 5.55913 14.0496 6.56141 14.2666 7.65255C14.4837 8.74369 14.3723 9.87469 13.9465 10.9025C13.5208 11.9304 12.7998 12.8089 11.8748 13.427C10.9497 14.045 9.86221 14.3749 8.74969 14.3749C7.25836 14.3733 5.82858 13.7801 4.77404 12.7256C3.71951 11.6711 3.12634 10.2413 3.12469 8.74993Z"
            fill="currentColor"
          />
        </svg>
      </button>

      <!-- Search Sidebar -->
      <aside v-if="isSearchSidebarOpen" class="pdf-search-sidebar">
        <form class="pdf-search-input-wrapper" @submit.prevent="submitSearch">
          <input
            ref="searchInputRef"
            v-model="searchKeyword"
            type="text"
            class="pdf-search-input"
            placeholder="Search... (press Enter)"
          />
        </form>

        <p v-if="isSearching" class="pdf-search-loading">Searching...</p>
        <p v-else class="pdf-search-summary">
          Found {{ searchResults.totalMatches }} match{{
            searchResults.totalMatches === 1 ? "" : "es"
          }}
        </p>

        <div v-if="searchResults.totalMatches > 0" class="pdf-search-results">
          <section
            v-for="group in matchesGroupedByPage"
            :key="group.pageNumber"
            class="pdf-search-page-group"
          >
            <h4 class="pdf-search-page-header">Page {{ group.pageNumber }}</h4>
            <div class="pdf-search-matches">
              <button
                v-for="match in group.matches"
                :key="match.matchNumber"
                type="button"
                class="pdf-search-match-btn"
                @click="openMatch(match.matchNumber)"
              >
                Match #{{ match.matchNumber }}
              </button>
            </div>
          </section>
        </div>
      </aside>
    </div>
  </div>
</template>

<!-- Style -->
<style scoped>
#vpv {
  width: 100%;
  height: 700px;
  margin: 0 auto;
}

.pdf-viewer-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

.pdf-search-toggle-btn {
  position: absolute;
  top: 96px;
  left: 8px;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  cursor: pointer;
}

.pdf-search-toggle-btn:hover,
.pdf-search-toggle-btn.is-active {
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.1);
}

.pdf-search-sidebar {
  position: absolute;
  top: 48px;
  left: 50px;
  bottom: 0;
  z-index: 15;
  box-sizing: border-box;
  width: 300px;
  max-width: min(300px, 80vw);
  background: #fafafa;
  padding: 12px;
  border-right: 1px solid #e5e7eb;
  box-shadow: 4px 0 14px rgba(15, 23, 42, 0.08);
  overflow-y: auto;
}

.pdf-search-input-wrapper {
  margin-bottom: 8px;
}

.pdf-search-input {
  width: 94%;
  border: 1px solid #bbb;
  border-radius: 6px;
  padding: 8px;
}

.pdf-search-loading,
.pdf-search-summary {
  margin: 0 0 10px;
  font-size: 13px;
}

.pdf-search-results {
  display: grid;
  gap: 12px;
}

.pdf-search-page-group {
  border-top: 1px solid #e3e3e3;
  padding-top: 8px;
}

.pdf-search-page-header {
  margin: 0 0 8px;
  font-size: 13px;
}

.pdf-search-matches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.pdf-search-match-btn {
  border: 1px solid #9ca3af;
  border-radius: 6px;
  background: #fff;
  padding: 4px 8px;
  font-size: 12px;
  cursor: pointer;
}

.pdf-search-match-btn:hover {
  background: #f3f4f6;
}
</style>
vue
<!-- Script -->
<script setup>
import { computed, nextTick, ref } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";

const toolbarOptions = { searchable: false };
const pdfViewerRef = ref(null);
const searchInputRef = ref(null);
const searchKeyword = ref("");
const isSearchSidebarOpen = ref(false);

// Return the search control object
const searchControl = computed(() => pdfViewerRef.value?.searchControl);
// Return the searching state
const isSearching = computed(() => searchControl.value?.searching ?? false);

// Return the search results
const searchResults = computed(() => {
  const searchMatchesSource = searchControl.value?.searchMatches;
  const rawMatches = searchMatchesSource?.matches ?? [];
  const matches = rawMatches.map((match, index) => ({
    pageNumber: match.page,
    matchNumber: index + 1, // goToMatch expects a 1-based match index
  }));

  return {
    totalMatches: searchMatchesSource?.totalMatches ?? 0,
    matches,
  };
});

// Return the matches grouped by page
const matchesGroupedByPage = computed(() => {
  const pageGroups = new Map();
  // Group by page, then sort page sections in ascending order
  for (const match of searchResults.value.matches) {
    const pageMatches = pageGroups.get(match.pageNumber) ?? [];
    pageMatches.push(match);
    pageGroups.set(match.pageNumber, pageMatches);
  }

  return [...pageGroups.entries()]
    .sort(([leftPage], [rightPage]) => leftPage - rightPage)
    .map(([pageNumber, matches]) => ({ pageNumber, matches }));
});

const submitSearch = () => {
  searchControl.value?.search(searchKeyword.value.trim());
};

const openMatch = (matchNumber) => {
  searchControl.value?.goToMatch(matchNumber);
};

const toggleSearchSidebar = () => {
  isSearchSidebarOpen.value = !isSearchSidebarOpen.value;
  // Focus the search input if the search sidebar is opened
  if (isSearchSidebarOpen.value) {
    void nextTick(() => {
      searchInputRef.value?.focus();
    });
  }
};
</script>

<!-- Template -->
<template>
  <div id="vpv">
    <div class="pdf-viewer-wrapper">
      <!-- PDF Viewer -->
      <VPdfViewer
        ref="pdfViewerRef"
        src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
        :toolbar-options="toolbarOptions"
      />

      <!-- Search Toggle Button -->
      <button
        type="button"
        class="pdf-search-toggle-btn"
        :class="{ 'is-active': isSearchSidebarOpen }"
        title="Toggle search panel"
        @click="toggleSearchSidebar"
      >
        <span aria-hidden="true">🔍</span>
      </button>

      <!-- Search Sidebar -->
      <aside v-if="isSearchSidebarOpen" class="pdf-search-sidebar">
        <form class="pdf-search-input-wrapper" @submit.prevent="submitSearch">
          <input
            ref="searchInputRef"
            v-model="searchKeyword"
            type="text"
            class="pdf-search-input"
            placeholder="Search... (press Enter)"
          />
        </form>

        <p v-if="isSearching" class="pdf-search-loading">Searching...</p>
        <p v-else class="pdf-search-summary">
          Found {{ searchResults.totalMatches }} match{{
            searchResults.totalMatches === 1 ? "" : "es"
          }}
        </p>

        <div v-if="searchResults.totalMatches > 0" class="pdf-search-results">
          <section
            v-for="group in matchesGroupedByPage"
            :key="group.pageNumber"
            class="pdf-search-page-group"
          >
            <h4 class="pdf-search-page-header">Page {{ group.pageNumber }}</h4>
            <div class="pdf-search-matches">
              <button
                v-for="match in group.matches"
                :key="match.matchNumber"
                type="button"
                class="pdf-search-match-btn"
                @click="openMatch(match.matchNumber)"
              >
                Match #{{ match.matchNumber }}
              </button>
            </div>
          </section>
        </div>
      </aside>
    </div>
  </div>
</template>

<!-- Style -->
<style scoped>
#vpv {
  width: 100%;
  height: 700px;
  margin: 0 auto;
}

.pdf-viewer-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

.pdf-search-toggle-btn {
  position: absolute;
  top: 96px;
  left: 8px;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  cursor: pointer;
}

.pdf-search-toggle-btn:hover,
.pdf-search-toggle-btn.is-active {
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.1);
}

.pdf-search-sidebar {
  position: absolute;
  top: 48px;
  left: 50px;
  bottom: 0;
  z-index: 15;
  box-sizing: border-box;
  width: 300px;
  max-width: min(300px, 80vw);
  background: #fafafa;
  padding: 12px;
  border-right: 1px solid #e5e7eb;
  box-shadow: 4px 0 14px rgba(15, 23, 42, 0.08);
  overflow-y: auto;
}

.pdf-search-input-wrapper {
  margin-bottom: 8px;
}

.pdf-search-input {
  width: 94%;
  border: 1px solid #bbb;
  border-radius: 6px;
  padding: 8px;
}

.pdf-search-loading,
.pdf-search-summary {
  margin: 0 0 10px;
  font-size: 13px;
}

.pdf-search-results {
  display: grid;
  gap: 12px;
}

.pdf-search-page-group {
  border-top: 1px solid #e3e3e3;
  padding-top: 8px;
}

.pdf-search-page-header {
  margin: 0 0 8px;
  font-size: 13px;
}

.pdf-search-matches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.pdf-search-match-btn {
  border: 1px solid #9ca3af;
  border-radius: 6px;
  background: #fff;
  padding: 4px 8px;
  font-size: 12px;
  cursor: pointer;
}

.pdf-search-match-btn:hover {
  background: #f3f4f6;
}
</style>
vue
<!-- Script -->
<script lang="ts">
import { defineComponent, computed, nextTick, ref } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";

// Type of the search match object
type SearchMatch = {
  pageNumber: number;
  matchNumber: number;
};

// Type of the search control object
type SearchControl = {
  goToMatch: (matchNumber: number) => void;
  search: (value: string) => void;
  searching?: boolean;
  searchMatches?: {
    totalMatches: number;
    matches: Array<{ index: number; page: number }>;
  };
};

export default defineComponent({
  components: {
    VPdfViewer,
  },
  setup() {
    const toolbarOptions = { searchable: false } as const;
    const pdfViewerRef = ref<InstanceType<typeof VPdfViewer>>();
    const searchInputRef = ref<HTMLInputElement | null>(null);
    const searchKeyword = ref("");
    const isSearchSidebarOpen = ref(false);

    // Return the search control object
    const searchControl = computed(
      () => pdfViewerRef.value?.searchControl as SearchControl | undefined
    );
    // Return the searching state
    const isSearching = computed(() => searchControl.value?.searching ?? false);

    // Return the search results
    const searchResults = computed(() => {
      const searchMatchesSource = searchControl.value?.searchMatches;
      const rawMatches = searchMatchesSource?.matches ?? [];
      const matches = rawMatches.map((match, index) => ({
        pageNumber: match.page,
        matchNumber: index + 1, // goToMatch expects a 1-based match index
      }));

      return {
        totalMatches: searchMatchesSource?.totalMatches ?? 0,
        matches,
      };
    });

    // Return the matches grouped by page
    const matchesGroupedByPage = computed(() => {
      const pageGroups = new Map<number, SearchMatch[]>();
      // Group by page, then sort page sections in ascending order
      for (const match of searchResults.value.matches) {
        const pageMatches = pageGroups.get(match.pageNumber) ?? [];
        pageMatches.push(match);
        pageGroups.set(match.pageNumber, pageMatches);
      }

      return [...pageGroups.entries()]
        .sort(([leftPage], [rightPage]) => leftPage - rightPage)
        .map(([pageNumber, matches]) => ({ pageNumber, matches }));
    });

    const submitSearch = () => {
      searchControl.value?.search(searchKeyword.value.trim());
    };

    const openMatch = (matchNumber: number) => {
      searchControl.value?.goToMatch(matchNumber);
    };

    const toggleSearchSidebar = () => {
      isSearchSidebarOpen.value = !isSearchSidebarOpen.value;
      // Focus the search input if the search sidebar is opened
      if (isSearchSidebarOpen.value) {
        void nextTick(() => {
          searchInputRef.value?.focus();
        });
      }
    };

    return {
      toolbarOptions,
      pdfViewerRef,
      searchInputRef,
      searchKeyword,
      isSearchSidebarOpen,
      isSearching,
      searchResults,
      matchesGroupedByPage,
      submitSearch,
      openMatch,
      toggleSearchSidebar,
    };
  },
});
</script>

<!-- Template -->
<template>
  <div id="vpv">
    <div class="pdf-viewer-wrapper">
      <!-- PDF Viewer -->
      <VPdfViewer
        ref="pdfViewerRef"
        src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
        :toolbar-options="toolbarOptions"
      />

      <!-- Search Toggle Button -->
      <button
        type="button"
        class="pdf-search-toggle-btn"
        :class="{ 'is-active': isSearchSidebarOpen }"
        title="Toggle search panel"
        @click="toggleSearchSidebar"
      >
        <span aria-hidden="true">🔍</span>
      </button>

      <!-- Search Sidebar -->
      <aside v-if="isSearchSidebarOpen" class="pdf-search-sidebar">
        <form class="pdf-search-input-wrapper" @submit.prevent="submitSearch">
          <input
            ref="searchInputRef"
            v-model="searchKeyword"
            type="text"
            class="pdf-search-input"
            placeholder="Search... (press Enter)"
          />
        </form>

        <p v-if="isSearching" class="pdf-search-loading">Searching...</p>
        <p v-else class="pdf-search-summary">
          Found {{ searchResults.totalMatches }} match{{
            searchResults.totalMatches === 1 ? "" : "es"
          }}
        </p>

        <div v-if="searchResults.totalMatches > 0" class="pdf-search-results">
          <section
            v-for="group in matchesGroupedByPage"
            :key="group.pageNumber"
            class="pdf-search-page-group"
          >
            <h4 class="pdf-search-page-header">Page {{ group.pageNumber }}</h4>
            <div class="pdf-search-matches">
              <button
                v-for="match in group.matches"
                :key="match.matchNumber"
                type="button"
                class="pdf-search-match-btn"
                @click="openMatch(match.matchNumber)"
              >
                Match #{{ match.matchNumber }}
              </button>
            </div>
          </section>
        </div>
      </aside>
    </div>
  </div>
</template>

<!-- Style -->
<style scoped>
#vpv {
  width: 100%;
  height: 700px;
  margin: 0 auto;
}

.pdf-viewer-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

.pdf-search-toggle-btn {
  position: absolute;
  top: 96px;
  left: 8px;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  cursor: pointer;
}

.pdf-search-toggle-btn:hover,
.pdf-search-toggle-btn.is-active {
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.1);
}

.pdf-search-sidebar {
  position: absolute;
  top: 48px;
  left: 50px;
  bottom: 0;
  z-index: 15;
  box-sizing: border-box;
  width: 300px;
  max-width: min(300px, 80vw);
  background: #fafafa;
  padding: 12px;
  border-right: 1px solid #e5e7eb;
  box-shadow: 4px 0 14px rgba(15, 23, 42, 0.08);
  overflow-y: auto;
}

.pdf-search-input-wrapper {
  margin-bottom: 8px;
}

.pdf-search-input {
  width: 94%;
  border: 1px solid #bbb;
  border-radius: 6px;
  padding: 8px;
}

.pdf-search-loading,
.pdf-search-summary {
  margin: 0 0 10px;
  font-size: 13px;
}

.pdf-search-results {
  display: grid;
  gap: 12px;
}

.pdf-search-page-group {
  border-top: 1px solid #e3e3e3;
  padding-top: 8px;
}

.pdf-search-page-header {
  margin: 0 0 8px;
  font-size: 13px;
}

.pdf-search-matches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.pdf-search-match-btn {
  border: 1px solid #9ca3af;
  border-radius: 6px;
  background: #fff;
  padding: 4px 8px;
  font-size: 12px;
  cursor: pointer;
}

.pdf-search-match-btn:hover {
  background: #f3f4f6;
}
</style>
vue
<!-- Script -->
<script>
import { computed, defineComponent, nextTick, ref } from "vue";
import { VPdfViewer } from "@vue-pdf-viewer/viewer";

export default defineComponent({
  components: {
    VPdfViewer,
  },
  setup() {
    const toolbarOptions = { searchable: false };
    const pdfViewerRef = ref(null);
    const searchInputRef = ref(null);
    const searchKeyword = ref("");
    const isSearchSidebarOpen = ref(false);

    // Return the search control object
    const searchControl = computed(() => pdfViewerRef.value?.searchControl);
    // Return the searching state
    const isSearching = computed(() => searchControl.value?.searching ?? false);

    // Return the search results
    const searchResults = computed(() => {
      const searchMatchesSource = searchControl.value?.searchMatches;
      const rawMatches = searchMatchesSource?.matches ?? [];
      const matches = rawMatches.map((match, index) => ({
        pageNumber: match.page,
        matchNumber: index + 1, // goToMatch expects a 1-based match index
      }));

      return {
        totalMatches: searchMatchesSource?.totalMatches ?? 0,
        matches,
      };
    });

    // Return the matches grouped by page
    const matchesGroupedByPage = computed(() => {
      const pageGroups = new Map();
      // Group by page, then sort page sections in ascending order
      for (const match of searchResults.value.matches) {
        const pageMatches = pageGroups.get(match.pageNumber) ?? [];
        pageMatches.push(match);
        pageGroups.set(match.pageNumber, pageMatches);
      }

      return [...pageGroups.entries()]
        .sort(([leftPage], [rightPage]) => leftPage - rightPage)
        .map(([pageNumber, matches]) => ({ pageNumber, matches }));
    });

    const submitSearch = () => {
      searchControl.value?.search(searchKeyword.value.trim());
    };

    const openMatch = (matchNumber) => {
      searchControl.value?.goToMatch(matchNumber);
    };

    const toggleSearchSidebar = () => {
      isSearchSidebarOpen.value = !isSearchSidebarOpen.value;
      // Focus the search input if the search sidebar is opened
      if (isSearchSidebarOpen.value) {
        void nextTick(() => {
          searchInputRef.value?.focus();
        });
      }
    };

    return {
      toolbarOptions,
      pdfViewerRef,
      searchInputRef,
      searchKeyword,
      isSearchSidebarOpen,
      isSearching,
      searchResults,
      matchesGroupedByPage,
      submitSearch,
      openMatch,
      toggleSearchSidebar,
    };
  },
});
</script>

<!-- Template -->
<template>
  <div id="vpv">
    <div class="pdf-viewer-wrapper">
      <!-- PDF Viewer -->
      <VPdfViewer
        ref="pdfViewerRef"
        src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
        :toolbar-options="toolbarOptions"
      />

      <!-- Search Toggle Button -->
      <button
        type="button"
        class="pdf-search-toggle-btn"
        :class="{ 'is-active': isSearchSidebarOpen }"
        title="Toggle search panel"
        @click="toggleSearchSidebar"
      >
        <span aria-hidden="true">🔍</span>
      </button>

      <!-- Search Sidebar -->
      <aside v-if="isSearchSidebarOpen" class="pdf-search-sidebar">
        <form class="pdf-search-input-wrapper" @submit.prevent="submitSearch">
          <input
            ref="searchInputRef"
            v-model="searchKeyword"
            type="text"
            class="pdf-search-input"
            placeholder="Search... (press Enter)"
          />
        </form>

        <p v-if="isSearching" class="pdf-search-loading">Searching...</p>
        <p v-else class="pdf-search-summary">
          Found {{ searchResults.totalMatches }} match{{
            searchResults.totalMatches === 1 ? "" : "es"
          }}
        </p>

        <div v-if="searchResults.totalMatches > 0" class="pdf-search-results">
          <section
            v-for="group in matchesGroupedByPage"
            :key="group.pageNumber"
            class="pdf-search-page-group"
          >
            <h4 class="pdf-search-page-header">Page {{ group.pageNumber }}</h4>
            <div class="pdf-search-matches">
              <button
                v-for="match in group.matches"
                :key="match.matchNumber"
                type="button"
                class="pdf-search-match-btn"
                @click="openMatch(match.matchNumber)"
              >
                Match #{{ match.matchNumber }}
              </button>
            </div>
          </section>
        </div>
      </aside>
    </div>
  </div>
</template>

<!-- Style -->
<style scoped>
#vpv {
  width: 100%;
  height: 700px;
  margin: 0 auto;
}

.pdf-viewer-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

.pdf-search-toggle-btn {
  position: absolute;
  top: 96px;
  left: 8px;
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  cursor: pointer;
}

.pdf-search-toggle-btn:hover,
.pdf-search-toggle-btn.is-active {
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.1);
}

.pdf-search-sidebar {
  position: absolute;
  top: 48px;
  left: 50px;
  bottom: 0;
  z-index: 15;
  box-sizing: border-box;
  width: 300px;
  max-width: min(300px, 80vw);
  background: #fafafa;
  padding: 12px;
  border-right: 1px solid #e5e7eb;
  box-shadow: 4px 0 14px rgba(15, 23, 42, 0.08);
  overflow-y: auto;
}

.pdf-search-input-wrapper {
  margin-bottom: 8px;
}

.pdf-search-input {
  width: 94%;
  border: 1px solid #bbb;
  border-radius: 6px;
  padding: 8px;
}

.pdf-search-loading,
.pdf-search-summary {
  margin: 0 0 10px;
  font-size: 13px;
}

.pdf-search-results {
  display: grid;
  gap: 12px;
}

.pdf-search-page-group {
  border-top: 1px solid #e3e3e3;
  padding-top: 8px;
}

.pdf-search-page-header {
  margin: 0 0 8px;
  font-size: 13px;
}

.pdf-search-matches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.pdf-search-match-btn {
  border: 1px solid #9ca3af;
  border-radius: 6px;
  background: #fff;
  padding: 4px 8px;
  font-size: 12px;
  cursor: pointer;
}

.pdf-search-match-btn:hover {
  background: #f3f4f6;
}
</style>