Building a custom PDF viewer in React with your own controls

build a customer pdf viewer in React with controls
build a customer pdf viewer in React with controls
build a customer pdf viewer in React with controls

Most dev teams rely on 3rd-party libraries when implementing PDF viewers in React applications. While this is a great strategy for teams that need to ship features fast, building a truly lightweight and custom PDF viewer with features like responsive viewports and toolbars for zooming and pagination might require a more hands-on approach.

Faced with the bloat of large PDF libraries, their limited customization options, and user interfaces that may clash with the look and feel of your application, the temptation to build your own PDF viewer from scratch is understandable. A DIY PDF viewer will give you complete control over its customization but it also requires a significant time investment that might be better spent on other development tasks.

Fortunately, you don’t have to choose between a restrictive library and rolling your own PDF viewer from scratch. You’re better off using a JavaScript library such as react-pdf, which offers the best of both worlds by providing a highly customizable PDF viewer for any React application.

This post shows React developers how to create a fully custom React PDF viewer with custom controls such as:

  • viewing remote and locally saved PDFs

  • a customized UI

  • zoom in/out

  • pagination

Create a new React App

As a first step, create a new React application by opening a terminal and running the following command. It should create a react-pdf-viewer folder that contains scaffolding for developing a React application.

npm create vite@latest -- --template react react-pdf-viewer

Next, navigate to the react-pdf-viewer folder and open it in your code editor or IDE.

Install dependencies

In the root directory of the react-pdf-viewer project, run the following terminal command to install react-pdf and material icons. Material icons will serve as intuitive labels for custom controls such as zoom and pagination buttons in the PDF viewer’s toolbar.

npm install react-pdf @fontsource-variable/material-symbols-outlined

Define the Application’s State

Users need to be able to navigate to other pages, zoom, see what page they’re on, and so forth when viewing a PDF document. Let’s define the initial state and how to manage changes to documents opened in the viewer by creating an src/state.js file with the following as its contents:

// src/state.js
export const initialState = {
  pdf: null, // PDF file as a { url: http/https URL to PDF file } or { data: UInt8Array of PDF file }
  numPages: null, // Total number of pages in PDF document
  pageNumber: 1, // Displays page 1 by default
  scale: 1.0, // Sets each page's dimensions to the document's page size by default
};

export function reducer(state, action) {
  switch (action.type) {
    case 'pdf':
      return { ...state, pdf: action.pdf };

    case 'num-pages':
      return { ...state, numPages: action.numPages };

    case 'arrow_left':
      if (state.pageNumber === 1) {
        return state;
      }
      return { ...state, pageNumber: state.pageNumber - 1 };

    case 'arrow_right':
      if (state.pageNumber === state.numPages) {
        return state;
      }
      return { ...state, pageNumber: state.pageNumber + 1 };

    case 'zoom_out':
      if (state.scale > 0.6) {
        return { ...state, scale: state.scale - 0.1 };
      }
      return state;

    case 'zoom_in':
      if (state.scale < 2.9) {
        return { ...state, scale: state.scale + 0.1 };
      }
      return state;

    default:
      return state;
  }
}

Create a PDFViewer Component

Create a PDFViewer.jsx file in the src folder of the project and copy the following code into it.

Add Custom Controls to the PDF Viewer

Although the above PDFViewer component is ready for use, it is currently missing the bells and whistles of most production-ready PDF viewers. Let's rectify that by creating a custom Toolbar and a ControlButton component to render the PDF viewer’s controls.

Create a Toolbar.jsx file in the src folder of the project and add the following components to it.

// src/Toolbar.jsx
import '@fontsource-variable/material-symbols-outlined';

function ControlButton({ label }) {
  return (
    <button
      className="toolbar-control material-symbols-outlined"
      type="button"
      data-action={label}
    >
      {label}
    </button>
  );
}

function Toolbar({ dispatch, pageNumber, numPages }) {
  const controls = ['arrow_right', 'zoom_in', 'zoom_out'];

  function dispatchControl(e) {
    const target = e.target;

    if (!e.target.className === 'toolbar-control') {
      return;
    }

    dispatch({ type: target.dataset.action });
  }

  return (
    <div className="toolbar" onClick={dispatchControl}>
      <ControlButton label="arrow_left" />
      <span class="page-number">{pageNumber}</span>/
      <span class="page-count">{numPages}</span>
      {controls.map((label, i) => (
        <ControlButton label={label} key={i} />
      ))}
    </div>
  );
}

export default Toolbar;

Passing a PDF File to the PDF Viewer

A PDF file is passed to react-pdf's Document component in one of two ways. It takes an object with a:

  • url field set to the target PDF file’s URL.

  • data field set to a Uint8Array .

Render a PDF from a URL

This method is ideal for situations where you need to render a PDF document located on the server or a remote file hosting service such as an S3 bucket.

For example, to show users a PDF document when they click a button, replace the contents of the src/App.jsx file with the following:

// src/App.jsx
import './App.css';
import { useReducer } from 'react';
import { reducer, initialState } from './state';
import PDFViewer from './PDFViewer';
import Toolbar from './Toolbar';

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function viewPdfFromUrl(e) {
    const target = e.target;

    if (!e.target.className === 'view-pdf-from-url') {
      return;
    }

    dispatch({
      type: 'pdf',
      pdf: { url: target.dataset.pdf },
    });
  }

  return (
    <>
      {state.pdf ? (
        <>
          <Toolbar
            dispatch={dispatch}
            pageNumber={state.pageNumber}
            numPages={state.numPages}
          />
          <PDFViewer
            file={state.pdf}
            pageNumber={state.pageNumber}
            scale={state.scale}
            dispatch={dispatch}
          />
        </>
      ) : (
        <button
          className="view-pdf-from-url"
          onClick={viewPdfFromUrl}
          data-pdf="<http://localhost:3001/dummy.pdf>"
        >
          View PDF
        </button>
      )}
    </>
  );
}

export default App;

Open a terminal at the root of the react-pdf-viewer folder and run npm run dev. Then visit the URL displayed in the terminal.

You should be able to click the View PDF button to view and navigate the contents of the http://localhost:3001/dummy.pdf file assuming you already have an HTTP server running on port 3001 of your computer that serves a dummy.pdf file.

The toolbar and dummy PDF file should (for now) appear similar to the following:

unstyled pdf viewer and toolbar

Render PDFs from the User's Computer

To allow users to view PDF documents stored locally on their devices, you can use a form with a file input element. To see this in action, create an src/PdfForm.jsx file and copy the following code into it:

// src/PdfForm.jsx
function PdfForm({ dispatch }) {
  async function getPdfByteArray(e) {
    // Get the File object of the uploaded PDF.
    const file = e.target.files[0];

    // Continue only if the user has uploaded a PDF document
    if (file.type !== 'application/pdf') {
      alert(`Error: ${file.name} is not a PDF file`);
      return;
    }

    // Read the contents of the PDF file from the user's device
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);

    // Handle error(s) encountered while reading the contents of the PDF file
    reader.onerror = () => {
      alert(`Unable to read ${file.name} to an ArrayBuffer`);
      console.error(reader.error);
    };

    // Wait till FileReader has read all contents
    // of the PDF file before proceeding
    reader.onload = async () => {
      // Transform the contents of the PDF file to a generic byte array
      const pdfByteArray = new Uint8Array(reader.result);

      // Set the Viewer component's file prop
      dispatch({ type: 'pdf', pdf: { data: pdfByteArray } });
    };
  }

  return (
    <form className="pdf-form">
      <label htmlFor="pdf-form__input" className="pdf-form__label">
        View a PDF file saved on your computer
      </label>
      <input
        onChange={async (e) => await getPdfByteArray(e)}
        type="file"
        name="pdf-file"
        id=""
        className="pdf-form__input"
      />
    </form>
  );
}

export default PdfForm;

If we replace the contents of the src/App.jsx file with the following, we should be able open and view locally saved PDF documents.

// src/App.jsx
import './App.css';
import { useReducer } from 'react';
import { reducer, initialState } from './state';
import PDFViewer from './PDFViewer';
import Toolbar from './Toolbar';
import PdfForm from './PdfForm';

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      {state.pdf && (
        <>
          <Toolbar
            dispatch={dispatch}
            pageNumber={state.pageNumber}
            numPages={state.numPages}
          />
          <PDFViewer
            file={state.pdf}
            pageNumber={state.pageNumber}
            dispatch={dispatch}
            scale={state.scale}
          />
        </>
      )}
      {!state.pdf && <PdfForm dispatch={dispatch} />}
    </>
  );
}

export default App;

Now that the PDF viewer works, you can apply your application’s theme to the page to stay on brand. However, since this post focuses on demonstrating how to customize the PDF viewer’s interface, the following steps should suffice:

  1. Delete or comment out the line containing import './index.css' in the src/main.jsx file.

  2. Replace the contents of the src/App.css file with the following:

    body,
    body *::before,
    body *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body,
    input,
    input::placeholder,
    button,
    .pdf-form__input::file-selector-button {
      font-family: sans-serif;
      color: #1C2025;
    }
    
    body {
      background-color: rgb(239, 246, 253);
      min-height: 100vh;
      padding: 0 1em 1em;
      max-width: 100%;
    }
    
    form {
      background-color: white;
      border-radius: 1em;
      padding: 4em 2em;
      max-width: 400px;
      margin: 25vh auto;
    }
    
    label {
      font-size: 1.5rem;
      display: block;
      margin-bottom: 12px;
    }
    
    .pdf-form__input {
      display: block;
    }
    
    .pdf-form__input::file-selector-button {
      border: 2px solid rgba(0, 0, 0, 0.1);
      border-radius: 0.5em;
      padding: 0.75em;
      font-size: 1rem;
      background-color: dodgerblue;
      color: rgb(247, 247, 247);
      cursor: pointer;
    }
    
    .toolbar {
      position: absolute;
      top: 0;
      right: 0;
      left: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 1.5rem;
      gap: 0.75em;
      padding: 0.5em;
    }
    
    .toolbar-control {
      cursor: pointer;
      font-family: 'Material Symbols Outlined Variable';
      font-size: 1.75rem;
      display: inline-block;
      line-height: 1;
      text-transform: none;
      letter-spacing: normal;
      word-wrap: normal;
      white-space: nowrap;
      direction: ltr;
      border: 1px solid #bbbbbb;
    }
    
    .react-pdf__Document {
      margin: 3.2em auto 0;
      max-width: fit-content;
    }

Now, reload the page. The viewer and toolbar should look similar to the following when you open a PDF file.

styled pdf viewer and toolbar

Using the react-pdf library to build a custom PDF viewer provides a solid foundation for creating a lightweight, tailored document viewing experience. This is largely because the library abstracts the complexity of rendering PDFs while still allowing developers to customize the document’s interface and controls.

This post has demonstrated how to render both remote and locally stored PDFs, as well as how to implement essential custom controls such as zoom in/out functionality and pagination. However, the possibilities for customization extend far beyond these basics when leveraging a lightweight and flexible library like react-pdf.

If this was just the beginning of your PDF build and you want to achieve more robust capabilities, check out Joyfill — a PDF form filler SDK that you can embed into your web or mobile app.

John Pagley

Published: Jun 26, 2025

Published: Jun 26, 2025