What you will find in this article?
PDF viewers have become essential components in many web applications. For instance, they are widely used in educational platforms, online libraries, and any other applications that involve document viewing. In this post, we will explore how we can create a beautiful page-by-page PDF viewer using react-pdf
.
Papermark - the open-source DocSend alternative.
Before we kick off, let me introduce you to Papermark. It's an open-source project for securely sharing documents that features a beautiful full-screen viewer for PDF documents.
I would be absolutely thrilled if you could give us a star! Don't forget to share your thoughts in the comments section ❤️
https://github.com/mfts/papermark
Setup the project
Let's set up our project environment. We will be setting up a Next.js app and installing the required libraries.
Set up tea
It's a good idea to have a package manager handy, like tea
. It'll handle your development environment and simplify your (programming) life!
sh <(curl https://tea.xyz)
# --- OR ---
# using brew
brew install teaxyz/pkgs/tea-cli
tea
frees you to focus on your code, as it takes care of installing node
, npm
, vercel
and any other packages you may need. The best part is, tea
installs all packages in a dedicated directory (default: ~/.tea
), keeping your system files neat and tidy.
Set up Next.js with TypeScript and Tailwindcss
We will use create-next-app to generate a new Next.js project. We will also be using TypeScript and Tailwind CSS, so make sure to select those options when prompted.
npx create-next-app
# ---
# you'll be asked the following prompts
What is your project named? my-app
Would you like to add TypeScript with this project? Y/N
# select `Y` for typescript
Would you like to use ESLint with this project? Y/N
# select `Y` for ESLint
Would you like to use Tailwind CSS with this project? Y/N
# select `Y` for Tailwind CSS
Would you like to use the `src/ directory` with this project? Y/N
# select `N` for `src/` directory
What import alias would you like configured? `@/*`
# enter `@/*` for import alias
Install react-pdf
There are actually two npm packages called "react-pdf": one is for displaying PDFs and one is for generating PDFs. Today we are focusing on the one to generate PDFs: https://github.com/wojtekmaj/react-pdf.
# Navigate to your Next.js repo
cd my-app
# Install react-pdf
npm install react-pdf
Building the application
Now that we have our setup in place, we are ready to start building our application.
Set up the PDF Viewer
The ability to programmatically configure pipes and datasources for Tinybird offers a significant advantage. This flexibility enables us to treat our data infrastructure as code, meaning that the entire configuration can be committed into a version control system. For an open-source project like Papermark, this capability is highly beneficial. It fosters transparency and collaboration, as contributors can readily understand the data structure without any ambiguity.
We set up Tinybird pipes and datasource as follows:
// components/pdfviewer.tsx
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
import { useEffect, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
export default function PDFViewer(props: any) {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1); // start on first page
const [loading, setLoading] = useState(true);
const [pageWidth, setPageWidth] = useState(0);
function onDocumentLoadSuccess({
numPages: nextNumPages,
}: {
numPages: number;
}) {
setNumPages(nextNumPages);
}
function onPageLoadSuccess() {
setPageWidth(window.innerWidth);
setLoading(false);
}
const options = {
cMapUrl: "cmaps/",
cMapPacked: true,
standardFontDataUrl: "standard_fonts/",
};
// Go to next page
function goToNextPage() {
setPageNumber((prevPageNumber) => prevPageNumber + 1);
}
function goToPreviousPage() {
setPageNumber((prevPageNumber) => prevPageNumber - 1);
}
return (
<>
<Nav pageNumber={pageNumber} numPages={numPages} />
<div
hidden={loading}
style={{ height: "calc(100vh - 64px)" }}
className="flex items-center"
>
<div
className={`flex items-center justify-between w-full absolute z-10 px-2`}
>
<button
onClick={goToPreviousPage}
disabled={pageNumber <= 1}
className="relative h-[calc(100vh - 64px)] px-2 py-24 text-gray-400 hover:text-gray-50 focus:z-20"
>
<span className="sr-only">Previous</span>
<ChevronLeftIcon className="h-10 w-10" aria-hidden="true" />
</button>
<button
onClick={goToNextPage}
disabled={pageNumber >= numPages!}
className="relative h-[calc(100vh - 64px)] px-2 py-24 text-gray-400 hover:text-gray-50 focus:z-20"
>
<span className="sr-only">Next</span>
<ChevronRightIcon className="h-10 w-10" aria-hidden="true" />
</button>
</div>
<div className="h-full flex justify-center mx-auto">
<Document
file={props.file}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className=""
>
<Page
className=""
key={pageNumber}
pageNumber={pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onLoadSuccess={onPageLoadSuccess}
onRenderError={() => setLoading(false)}
width={Math.max(pageWidth * 0.8, 390)}
/>
</Document>
</div>
</div>
</>
);
}
function Nav({pageNumber, numPages}: {pageNumber: number, numPages: number}) {
return (
<nav className="bg-black">
<div className="mx-auto px-2 sm:px-6 lg:px-8">
<div className="relative flex h-16 items-center justify-between">
<div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex flex-shrink-0 items-center">
<p className="text-2xl font-bold tracking-tighter text-white">
Papermark
</p>
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
<div className="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium">
<span>{pageNumber}</span>
<span className="text-gray-400"> / {numPages}</span>
</div>
</div>
</div>
</div>
</nav>
);
}
Let's break it down what's happening here:
From react-pdf
, we are using Document and Page component. In addition, we are loading the pre-packaged version of pdfjs to initialize a worker in the browser.
import { Document, Page, pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
Next, we are writing two function that 1) count the total number of pages (once) and 2) adjust the page width as needed (per page)
// ...
export default function PDFViewer(props: any) {
// useState variables
function onDocumentLoadSuccess({
numPages: nextNumPages,
}: {
numPages: number;
}) {
setNumPages(nextNumPages);
}
function onPageLoadSuccess() {
setPageWidth(window.innerWidth);
setLoading(false);
}
// ...
}
Next, we use the Document
and Page
components. This is the core of the PDF viewer. Important to note that Document
takes a file
prop, that can be a URL, base64 content, Uint8Array, and more. In my case, I'm loading a URL to a file.
// ...
export default function PDFViewer(props: any) {
// ...
return (
<div className="h-full flex justify-center mx-auto">
<Document
file={props.file}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className=""
>
<Page
className=""
key={pageNumber}
pageNumber={pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onLoadSuccess={onPageLoadSuccess}
onRenderError={() => setLoading(false)}
width={Math.max(pageWidth * 0.8, 390)}
/>
</Document>
</div>
)
}
We also added a navigation bar with showing the current page number and buttons for navigating to the next / previous page of the document.
You can use the PDF Viewer component anywhere in your application to visualize PDFs beautifully.
Tada 🎉 The PDF Viewer component is ready!
Conclusion
That's it! We've built a beautiful PDF Viewer component for displaying PDF documents using react-pdf, and Next.js.
Thank you for reading. I'm Marc, an open-source advocate. I am building papermark.io - the open-source alternative to DocSend with millisecond-accurate page analytics.
Help me out!
If you found this article helpful and got to understand react-pdf better, I would be eternally grateful if you could give us a star! And don't forget to share your thoughts in the comments ❤️
Top comments (8)
And this one adjusts to different screen sizes?
Yes that's exactly why I'm fetching the
page.width
on pageload :)Got it
does this have right to left support? What about Arabic letters?
absolutely! it renders the pdf with a html canvas element and the pdf document can be in any language or text direction :)
Great article, and amazing cover!
Thanks Nevo 🤩
Getting cors issue on local host, how can i resolve it?
Access to fetch at 'w3.org/WAI/ER/tests/xhtml/testfile...' from origin 'localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.