COMING SOON: Fall 2024 Release

How To Build a Customizable React PDF Viewer?

By Apryse | 2024 Oct 14

Sanity Image
Read time

8 min

In this blog, we will cover how to build a customizable and feature-rich PDF Viewer in React. We will be using the Apryse SDK - Apryse’s rendering engine - and building a custom UI to display PDFs, scroll pages, zoom, and search with annotation and highlighting support.

For many users though, the open-source and easily customizable Webviewer-UI might be more appropriate - with many features straight out of the box, so you may want to check those out.

Getting Started

Copied to clipboard

OK! If you are still reading then we'll go through the manual steps of setting up the UI.

We’ll assume that you already have a Node environment setup and you have a React app up that you want to add WebViewer to.

Alternatively, you can also just create a new React app (after setting up a Node environment), by running:

npm create vite@latest

then following the instructions.

If you are new to Vite and prefer a video, then see the first part of this video.


OK! Let’s get started adding PDF rendering to our app!

Installing a PDF Rendering Engine

Copied to clipboard

First, we need a robust rendering engine that will display PDFs. There are several open-source rendering engines available such as PDF.js and PDFium, however in this article we will use Apryse’s proprietary engine since it offers so much functionality.

Why is Apryse SDK such a powerful tool?

Copied to clipboard

Apryse’s proprietary engine comes with a lot of functionality, straight out-of-the-box. In addition to being great at rendering PDFs, it also supports:

Almost all functionality is performed directly within the browser (using WASM), making it extremely fast and keeping your documents secure rather than them being transferred over the internet and processed on a remote server.

Installing the Apryse Rendering Engine

Copied to clipboard

Probably the easiest way to get the Apryse rendering engine is to run:

npm i @pdftron/webviewer

Once the package has been installed, copy the `public/core` contents located in your `node_modules/@pdftron/webviewer/public/core` to the `/public/webviewer/core` folder in your React app.

We need to do this because the library runs in the browser, so it needs to be accessible from the client-side code that will run there.

Image showing the location of copied WebViewer files in your React project

Location of copied WebViewer files in your React project

Now, you can reference the necessary scripts for rendering between the <body> tags of `public/index.html`, similarly to how it is done in the GitHub repository for this project: https://github.com/PDFTron/webviewer-custom-ui/blob/master/public/index.html#L29

<script src="./webviewer/core/webviewer-core.min.js"></script>
<script src="./webviewer/core/pdf/PDFNet.js"></script>


Now, you should be able to reference it wherever you are building the PDF Viewer.

StrictMode in React 18.0

Copied to clipboard

If you are using React 18 (which is the current default version at the time of writing this article), then it is likely to implement StrictMode which causes the React Components to render twice during development.

While that does not occur with production builds, it can be very confusing during development, so you may wish to remove the <React.StrictMode> element from main.jsx.


Creating a React PDF Viewer Component

You can now create a reusable PDF Viewer component in your React app.

In our example, though, we will keep things simple and just add the code directly to `App.js`.

Let’s add where the PDF viewer will be rendered,as well as some classes and ids for styling. This is done in the return method of your component or App.js:

 return (
    <div className="App">
      <div id="main-column">
        <div className="flexbox-container" id="scroll-view" ref={scrollView}>
          <div id="viewer" ref={viewer}></div>
        </div>
      </div>
    </div>
  );

Next insert a `useEffect` in App.js (which should only run once upon rendering the component - but see the warning about StrictMode), and add the following:

function App() {
  const viewer = useRef(null);
  const scrollView = useRef(null);
  const [documentViewer, setDocumentViewer] = useState(null);
  const [annotationManager, setAnnotationManager] = useState(null);
  useEffect(() => {
    const Core = window.Core;
    Core.setWorkerPath('/webviewer/core');
    Core.enableFullPDF();
    const docViewer = new Core.DocumentViewer();
    docViewer.setScrollViewElement(scrollView.current);
    docViewer.setViewerElement(viewer.current);
    docViewer.enableAnnotations();
    docViewer.loadDocument('https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf');

    docViewer.addEventListener('documentLoaded', () => {
      // Store the annotation manager for later use
      setAnnotationManager(docViewer.getAnnotationManager());
    });

    setDocumentViewer(docViewer);
  }, []);
  
  // return method....
}


Okay, what is happening there?

  1. We load the Core rendering engine from Apryse.
  2. We set the path to the resources necessary for document rendering - in this case webviewer/core.
  3. We create a new document viewer and point it to the div element we created earlier.
  4. We then enable annotations and load our PDF from an s3 bucket. As analternative you could use a local file that was located in the `public` folder.
  5. Then we wait for an event that lets us know the viewer is ready to go and the document is loaded. Currently we just store the Annotation Manager into a State variable so that it can be accessed later, but you could do lots of other things in this event handler.
  6. Finally, we set the document viewer to the one we created.

Now, let’s add some CSS, to make sure the viewer looks its best. Inside of App.css (or your component’s CSS), add the following and do not forget to import it:

:root {
  --pdf-background: var(--grey-2);
  --tools-header-background: var(--grey-3);
}

.App {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: row;
  overflow: hidden;
}

.App .header {
  width: 100%;
  height: 100px;
  padding: 8px 8px 8px 16px;
  box-sizing: border-box;
  background: #00a5e4;
  font-size: 1.2em;
  line-height: 44px;
  color: white;
}

#scroll-view {
  bottom: 0;
  /* leave room for 100px header */
  height: 100%;
  width: 100%;
  overflow: auto;
  flex: 3;
  background-color: var(--pdf-background);
}

#viewer {
  margin: auto;
}

#main-column {
  flex-direction: row;
  flex: 3;
}

#tools {
  display:flex;
  flex-direction: row;
  justify-content: center;
  background-color: var(--tools-header-background);
}

#tools button {
  background-color: var(--tools-header-background);
  fill: #757575;
  width: fit-content;
  height: 35px;
  margin: 5px;
  cursor: pointer;
}

#tools button:hover {
  background-color: #c7d2dd;
  border-radius: 4px;
}

.flexbox-container {
  display: flex;
}

We also need to add some css to index.css, just to set the styles the way that we want.

:root {
  --grey-2: #F1F3F5;
  --grey-3: #E7EBEE;
}

html, body, #root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: Arial, Helvetica, sans-serif;
}

.webviewer {
  flex: 1;
  margin: 8px;
  -webkit-box-shadow: 1px 1px 10px #999;
          box-shadow: 1px 1px 10px #999;
}

At this point, we are ready to start our React app and see the PDF render:

npm run dev

WebViewer running in the browser, showing a PDF

WebViewer running in the browser

If you ran into any trouble, or something is going wrong, just refer to our GitHub sample - it's based on create-react-app rather than vite, but the principles are the same. If you are still stuck, chat with us on Discord.

If you are looking for a pre-built UI that is battle-tested by millions of developers, check out our other blog: https://apryse.com/blog/embed-react-pdf-viewer.

Add Annotation Support to Your PDF Viewer

Copied to clipboard

The next step, in this demo, is to allow users to draw annotations and comment on a PDF. Thanks to the Apryse Core rendering engine, WebViewer comes with out-of-the-box support for all annotation types supported in Adobe Reader. So, in this section, we are going to add a button to the UI that allows us to draw rectangles on a PDF document.

Inside the return method for App.js or your component, add the button tag just before the scroll view:

return (
  <div className="App">
    <div id="main-column">
      <div className="center" id="tools">
        <button id='btn' onClick={createRectangle}>Rectangle</button>
      </div>
      <div className="flexbox-container" id="scroll-view" ref={scrollView}>
        <div id="viewer" ref={viewer}></div>
      </div>
    </div>
  </div>
 );

Let’s now add the code that will set the tool to be a rectangle whenever that button is pressed.

Somewhere between useEffect and the return statement, add the following:

const createRectangle = () => {
  documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.RECTANGLE));
};

We have already added some basic CSS for the button, but feel free to play around with it to get it to match your overall theme.

Now, the button press will allow users to add rectangle annotations. Restart the app and try it yourself.

A button has been added to the app that allows the user to create rectangular annotations.

Clicking on the Rectangle button allows the user to add annotations.

Next Steps

Copied to clipboard

At this point, you can add more tool support, page manipulation, search, redaction and more. Check out the full sample available on GitHub and refer to our guides for more information.

Alternatively checkout the open-source WebViewer UI which gives you huge functionality straight out of the box. It now has a modular UI that gives you rapid customization abilities that will allow you to be WCAG 2.1 compliant.

This article was updated in October 2024.

Sanity Image

Apryse

Share this post

email
linkedIn
twitter