How to Create a PDF Viewer Using Next.js

Learn to Build a Scalable and Reusable React-Based PDF Solution.

Image

Introduction

Handling PDF documents in web apps isn’t as easy as it sounds. Even though frameworks like Next.js provide an excellent foundation for building modern web applications, integrating PDF viewing capabilities requires careful consideration of various factors such as performance, cross-browser compatibility, and feature support.

To address these challenges, developers often turn to libraries and SDKs specifically designed for handling PDFs — and while open-source options like PDF.js are popular for their accessibility and simplicity, if you have strict needs for advanced functionality, performance with large or complex documents, consistent rendering across devices, and security, they’re not going to hold up.

That’s why in this tutorial, I’m going to show you how to build a robust PDF viewer using Next.js and the Apryse WebViewer SDK, instead.

Whether you’re building a document management system, an e-learning platform, or any application that deals with PDFs, this tutorial will provide you with the knowledge to implement a reusable React-based PDF solution that can scale as your needs grow.

Let’s dive right in.

What is Apryse?

Formerly known as PDFTron, Apryse is a document processing technology company that provides comprehensive solutions for developers, enterprises, and small businesses. At its core, Apryse offers a suite of powerful tools and technologies designed to handle various aspects of document processing. The Apryse SDK offers an all-in-one solution that enables developers to integrate advanced document handling capabilities into applications.

These SDKs exist for a wide variety of platforms — desktop, mobile, client and server — and support a wide range of file formats, including PDF, Microsoft Office documents, images, and even CAD files, making it versatile for different use cases.

This tutorial will use their WebViewer SDK, which allows native document rendering, manipulation, annotation, and more — for any webapp you build, regardless of framework (in our case, a modern Next.js app). Entirely in the browser, no server necessary.

* The Internet Explorer JavaScript engine is noticeably slower than the other browsers, and may not give you the performance you desire.

But if you wish to check out the code directly, without reading through the article, visit this Github repo.

How to Create a PDF Viewer Using Next.js

Let’s begin by setting up the necessary components and configuring the WebViewer for optimal performance in your Next.js application.

Installing Necessary Packages

First of all, you need to create a new Next.js application by running the following command:

npx create-next-app@latest

Here’s what my configuration looks like:

✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a \`src/\` directory? … Yes
✔ Would you like to use App Router? (recommended) …  Yes
✔ Would you like to use Turbopack for next dev? … Yes
✔ Would you like to customize the import alias (@/\* by default)? … No

Now, for this tutorial, you’ll need only one package:

@pdftron/webviewer: This is the official package from Apryse for integrating the PDF viewer in JavaScript-based applications. The core of this package is written in C++ and compiled using WebAssembly for use in web applications.

To install the package, assuming you are using npm, run the following command from the terminal:

npm i @pdftron/webviewer

Once the application scaffolding and installing the package is complete, you can move to the next section for obtaining a trial key to try all the features of Apryse.

Obtaining API Key from Apryse

Apryse offers the trial key for free and without any expiry. So, you can test the features until you are completely satisfied with the product.

To obtain the key, first visit the Apryse website, and click on the Try Now button available on the top-right of the screen. Clicking on the button will redirect you to a page from where you can choose your application type and/or any framework or language. The docs will be shown according to your chosen language/framework.

Once you’ve successfully chosen the preferences and clicked on the Get Started button below the form, you’ll be redirected to the Apryse docs. Here, you’ll be prompted to log in to get access to the trial key.

Click on the Login to Reveal button and log in with a medium of your choice. It can be Google, Github or standard email/password. After logging in, you’ll be presented with a trial key. Create a new file named `.env.local` in your Next.js application’s root directory and copy this key into the file. The contents of your `.env.local` file should resemble the following:

# .env.local

NEXT_PUBLIC_APRYSE_KEY=demo:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Remember to replace the placeholder key with the actual trial key provided by Apryse.

Let’s move on to the next step.

Setting up static assets

Apryse needs to load some static assets for the WebViewer to run. To implement it and its PDF viewing capabilities into your Next.js application, you’ll need to make a few adjustments to your project setup.

Technically, for NextJS, the only thing you’ll need to do is copy everything located at `./node_modules/@pdftron/webviewer/public` to your `/public` folder (we’ll move it to `/public/webviewer` for better organization of our public assets folder)

But you can’t do that manually when you’re building and deploying your NextJS app, so what we’re going to do is include a new `package.json` script for moving these static assets.

In the scripts section of your package.json, add a new command called `move-static`:

"move-static": "cp -a ./node\_modules/@pdftron/webviewer/public/. ./public/webviewer"

Next, update the build command to run this new script before building the application:

"build": "npm run move-static && next build"

Your complete scripts section should now resemble the following:

"scripts": {
  "dev": "next dev --turbopack",
  "build": "npm run move-static && next build",
  "start": "next start",
  "lint": "next lint",
  "move-static": "cp -a ./node\_modules/@pdftron/webviewer/public/. ./public/webviewer"
}

All done. Now, during dev, run `npm run move-static` once to copy the required files to the `public` folder. Your build process should handle it automatically during production builds.

Moving on, you should see a new `webviewer` folder inside your `public` directory.

Creating a PDFViewer Component

With the static files in place, create a new folder called `components` inside the `src` directory. Within this folder, create a file named `PDFViewer.tsx`. This component will be responsible for rendering PDFs in the browser. Paste the code shown below into this newly created file:

"use client";

import { WebViewerOptions } from "@pdftron/webviewer";
import { useEffect, useRef } from "react";

const PDFViewer: React.FC = () => {
  const viewerRef = useRef`<HTMLDivElement | null>`(null);

  useEffect(() => {
    const loadWebViewer = async () => {
      const WebViewer = (await import("@pdftron/webviewer")).default;
      if (typeof window === "undefined" || !viewer.current) return;

      try {
        const config: WebViewerOptions = {
          path: "/webviewer",
          initialDoc: "/pdf.pdf",
          licenseKey: process.env.NEXT\_PUBLIC\_APRYSE\_KEY,
        };

        await WebViewer(config, viewerRef.current);
      } catch (error) {
        console.error("Failed to load WebViewer:", error);
        alert("Failed to load PDF viewer. Please try again later.");
      }
    };

    loadWebViewer();
  }, \[\]);

  return (
    `<div className="flex h-screen w-full flex-col bg-gray-50">`
      `<div
        ref={viewerRef}
        className="relative h-full w-full overflow-hidden rounded-lg shadow-lg"
      />`
    `</div>`
  );
};

export default PDFViewer;

The code is straightforward. The component is defined as a client-side component using the `use client` directive. Only the necessary hooks (`useEffect` and `useRef`) and a TypeScript interface (`WebViewerConfig`) are imported. Here’s a quick rundown:

  • We initialise a viewer reference using `useRef`, which points to a `div` where the PDF viewer will be embedded.
  • Inside the `useEffect` hook, an asynchronous function `loadWebViewer` is defined and immediately called upon mounting, dynamically importing the `WebViewer` module only on the client side to avoid errors.
  • The `WebViewer` is configured with `WebViewerConfig`, specifying the path to the `WebViewer` library, the initial document to display, and a license key, where the trial key is used.
  • The `path` should be set to the location where the `WebViewer` files were copied by using the command `npm run move-static`.
  • Our initial document, in this case, is a PDF stored locally in the `public` directory. You can also provide a URL here of the document you want to load.

Import this component into your root page, i.e., the `src/app/page.tsx` file. Here’s how it should look:

import PDFViewer from "@/components/PDFViewer";

export default function Home() {
  return `<div>``<PDFViewer />``</div>`;
}

If you visit your application now, you should be able to see the PDF viewer taking up the whole screen.

Your PDF viewer is now ready!

The WebViewer — explained

With the WebViewer integrated, you’ve instantly added a powerful document viewer to your NextJS app with just a few lines of code. It includes a versatile menu with options to download, save, print, toggle full-screen, and access a full settings panel featuring light/dark modes, fully rebindable keyboard shortcuts, and other customizable preferences.

But there’s more! All of which you need to add zero code for, and all of which you can fully customize according to your needs. Let’s have a quick look.

1. Built in zoom, pan, and select

So your users can scale the document for closer examination or fit-to-screen views, with smooth pinch-to-zoom (on touch devices) and zoom slider controls. Intuitive drag-to-navigate, and the Select menu has its own contextual menu in the PDF area, supporting shortcuts to annotate, edit, or insert notes. Speaking of which….

2. Annotation

The Annotate menu will give your users access to apply Highlights to emphasize text/sections, underline to mark key points or phrases with precise underlining, strikethrough to cross out text to indicate revisions/deletions, the freeform pen, text, or sticky notes for manual annotation — and lend themselves well to Google Docs-style real-time collaborative feedback, which the WebViewer includes native support for.

3. Native thumbnails/outlines/bookmarks for your PDF, with easy navigation

Native thumbnails for quick page previews, structured outlines for jumping to specific sections, and bookmarks to easily revisit important content. Combined with smooth scrolling and intuitive controls, these tools work right out of the box — no additional dependencies or plugins needed.

4. Advanced search for the whole document

Baked-in search to quickly locate text across the entire document. With features like keyword highlighting, case-sensitive matching, regex, and result previews with instant navigation, your users can find what they need with precision and ease with no extra work on your part.

6. Stamps, Watermarks, and Signatures, fully editable

Add and customize stamps, watermarks, and signatures directly within your PDF for branding, compliance, or confidentiality, and add digital or handwritten signatures. All elements are fully editable and losslessly transformable, allowing adjustments to placement, size, and style for complete flexibility.

7. Redaction

Easily redact sensitive information by clicking and dragging a bounding box around any content you want to hide, or by simply highlighting text for removal. The tool also allows for redaction of entire pages or specific page ranges, ensuring flexibility in securing your documents.

As you can see, the WebViewer SDK provides a comprehensive suite of tools, from an advanced search, to multiple forms of annotation, and even freeform redaction.

Building these functionalities from scratch or relying on a basic library like PDF.js would demand significant time, development effort, and a deep understanding of PDF rendering and interaction. With the WebViewer, though, you can skip the complexity and implement these features instantly — we’ve got all of this functionality with just a few lines of code, after all — saving both time and resources while ensuring a robust, professional-grade experience for your users.

Customizing the WebViewer UI

WebViewer works by creating a WebComponent or iFrame in the viewer element you’ve defined in your app code, with the `src` set to the WebViewer UI’s index.html

WebViewer looks great out of the box, but it is also easy to customize.

The easiest way to do this is by using the Modular UI which was released in version 11 in November 2024. It allows you to customize WebViewer in a quintessentially “React” way: by using reusable, modular components to adjust styles, modify existing UI elements, or create your own WebViewer interface from scratch.

💡 The modular elements here are WCAG 2.1 AA compliant — essential if you have customers where this is a legal requirement.

For more detailed information on customizing the WebViewer UI using the Modular UI, you can refer to this blog post.

If you want even more control, WebViewer provides a Core Engine API that lets you interact directly with the underlying PDF engine — and implement your own navigation, annotation tools, and other UI without relying on the prebuilt interface. Covering this is beyond the scope of this tutorial, but for details and examples on using the Core Engine API, check out the official guide here.

Conclusion

Implementing a powerful, customizable, and yet, completely native PDF viewer in your Next.js application sounds difficult — but using Apryse WebViewer, it becomes a remarkably straightforward process.

While a commercial solution, I’d still choose Apryse over PDF.js with its faster WebAssembly-based rendering, advanced features like annotations, form filling, and editing, and consistent cross-platform performance — not to mention how easy it was to add so much enterprise-ready (and scalable) functionality to an app with just a few lines of code.

It comes down to this — you’re investing in a reliable, well-supported platform that can grow with your application’s needs.

For further reading, you can go ahead and explore the Apryse API documentation to discover more ways to enhance, customise, and adapt your PDF viewer as needed to create powerful, feature-rich document processing experiences.

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics