npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-web-pdf-editor

v0.1.6

Published

React PDF editor library for editing PDFs in the browser. Add text, annotations, images, signatures, and export edited PDF documents.

Readme

react-web-pdf-editor

React PDF editor library for editing PDFs in the browser. Add text, annotations, images, and signatures, then export the edited PDF. Open a file, change the page order, and download the result. Rendering uses PDF.js. Export uses pdf-lib.

Live demos open the same sample PDF three ways: a direct client import, a browser-only dynamic import, and a toolbar you render yourself.

The editor includes:

  • Continuous page viewing, zoom, and in-document search
  • Text boxes, freehand ink, highlights, images, and signatures
  • A properties panel for the selected object
  • Page thumbnails, blank pages, delete, and drag-to-reorder
  • Undo and redo
  • Export that flattens your edits into a downloadable PDF

Annotations stay in memory until you export. Changing the file prop starts a fresh document and clears them.

Demos

The examples site loads /file/compressed.tracemonkey-pldi-09.pdf on every route. Replace that URL with your own PDF when you copy the code.

| Example | Live page | What it shows | | --- | --- | --- | | Quick start | /quick-start | A client component imports PdfEditor, loads the stylesheet, and opens a PDF with a document name and a full-viewport height (file, name, and style). | | Basic demo | /basic-demo | next/dynamic loads PdfEditor with ssr: false, so canvas, window, and the PDF.js worker run in the browser. Safe to render from an App Router page. | | Custom toolbar | /custom | A toolbar render prop. The bar renames the document, switches tools, and exports through ToolbarApi. |

The custom toolbar covers select, draw, highlight, text, image, and signature, plus undo, redo, zoom, pages, search, and export. The quick start, Next.js, and custom toolbar sections below are those same patterns.

Install

npm install react-web-pdf-editor

Peer dependencies: react and react-dom 18 or later.

Import the stylesheet once. Without it, the toolbar, pages, and page canvas have no layout.

import { PdfEditor } from "react-web-pdf-editor";
import "react-web-pdf-editor/styles.css";

Quick start

This is the quick start demo: a client component that imports PdfEditor directly.

import { PdfEditor } from "react-web-pdf-editor";
import "react-web-pdf-editor/styles.css";

export function App() {
  return <PdfEditor file="/contract.pdf" name="Service agreement" />;
}

file accepts a URL string, URL, File, Blob, ArrayBuffer, or Uint8Array. Leave it empty to show the empty state.

Open a file from the user's computer

import { useState } from "react";
import { PdfEditor, type PdfSource } from "react-web-pdf-editor";
import "react-web-pdf-editor/styles.css";

export function App() {
  const [file, setFile] = useState<PdfSource | null>(null);

  return (
    <div style={{ height: "100vh" }}>
      <input
        type="file"
        accept="application/pdf"
        onChange={(event) => {
          const next = event.target.files?.[0];
          if (next) {
            setFile(next);
          }
        }}
      />
      <PdfEditor file={file} style={{ height: "calc(100vh - 40px)" }} />
    </div>
  );
}

Give the editor a height. The root fills its parent (height: 100%), so a parent with no height collapses the canvas.

Remote URLs are fetched in the browser, with a download progress state when the server sends Content-Length. The PDF host must allow your origin through CORS. A File from an <input> does not have that restriction.

Using the editor

The built-in chrome has a toolbar, a page-thumbnail sidebar, the document, and a properties panel. The toolbar holds the document name, tools, search, zoom, the page count, and Export PDF. The sidebar lists every page and ends with Add blank page. The properties panel stays open on the right; until something is selected it reads “Select a tool or annotation to edit properties.”

On widths under 900px the same editor switches to a compact layout: a slim top bar, a bottom tool dock, pages in a left drawer, and properties in a sheet.

Tools

| Tool | What it does | | --- | --- | | Select | Move, resize, and rotate text, ink, highlights, images, and signatures. Click empty canvas to clear the selection. | | Text | Click the page to place a box, then type. Double-click an existing box to edit it. | | Draw | Draw freehand ink. Pick a color and a width (thin, pen, marker, bold) in the properties panel. | | Highlight | Drag across existing PDF text to mark those lines, or drag on a blank area to paint a rectangle. | | Signature | Opens a dialog to draw, type, or upload a signature, then click the page to place it. | | Image | Opens a dialog to choose a PNG, JPG, or WebP, then click the page to place it. |

Select an object to edit it in the properties panel:

  • Text: content, font, size, color, bold, italic, alignment, rotation
  • Ink: color and stroke width
  • Highlight: color
  • Image: opacity, rotation, and replace
  • Signature: rotation

Text fonts in the panel are Arial, Helvetica, Times New Roman, Georgia, Courier New, and Verdana. On export, text is painted as it appears on screen so the saved PDF matches the editor.

Images are limited to 8 MB and are scaled so the longest edge is at most 2400 pixels. Signatures can be drawn, typed, or uploaded (PNG, JPG, or WebP, up to 4 MB). Typed signatures use script fonts loaded from Google Fonts, so a strict Content-Security-Policy that blocks fonts.googleapis.com will fall back to a plainer face. The dialog keeps up to five recent signatures in localStorage on this browser.

Press Delete or Backspace to remove the selected object. Press Escape to leave the current tool, close a compact drawer, or clear the selection.

Pages

The sidebar shows a thumbnail for every page, including your annotations.

  • Click a thumbnail to jump to that page.
  • Use Add blank page to insert a blank page after the current page.
  • Delete a page from its thumbnail. The last remaining page cannot be deleted.
  • Drag a thumbnail to reorder. With a thumbnail focused, Alt+Arrow also moves it.

Page insert, delete, and reorder rewrite the working PDF. They are disabled while a page change is still saving.

Find, zoom, and rename

Search is in the toolbar. Ctrl+F or Cmd+F focuses it. Enter jumps to the next match, Shift+Enter to the previous one, and Escape clears it. Matches scroll into view.

Zoom from the toolbar, with Ctrl or Cmd plus + / -, or with Ctrl or Cmd and the mouse wheel while the pointer is over the editor. The default range is 50% to 300%, starting at 100%.

The document name in the toolbar is editable. Click it, type a new name, and press Enter. That name is the default export filename. Characters that are illegal in file names (/ \ ? % * : | " < >) are stripped.

Export

Export PDF in the toolbar builds a PDF and saves it. The file includes the current page order and every annotation flattened onto the pages. If nothing has been annotated, export returns the current PDF bytes, including any page changes.

The download name is the document name, with .pdf added when it is missing. Set exportFileName when the download name should differ from the name shown in the toolbar.

Export runs in a worker and times out after 60 seconds. If the worker cannot start, export continues on the main thread.

Keyboard shortcuts

These apply while focus is inside the editor and you are not typing in a text field or dialog.

| Shortcut | Action | | --- | --- | | Ctrl+Z / Cmd+Z | Undo | | Ctrl+Y or Ctrl+Shift+Z / Cmd+Shift+Z | Redo | | Ctrl+F / Cmd+F | Find | | Ctrl or Cmd + + / - | Zoom in / out | | Ctrl or Cmd + mouse wheel | Zoom | | Delete / Backspace | Delete the selected object | | Escape | Close a drawer, stop editing, or leave the current tool | | Left / Right | Previous / next page | | Up / Down | Scroll the page | | Page Up / Page Down | Scroll about one screen | | Home / End | Jump to the top or bottom of the document |

Undo covers annotation edits, page changes, and renames.

Props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | file | PdfSource \| null | — | PDF to open. Omit it, or pass null, to show the empty state. | | name | string | File name, or "document.pdf" | Document name in the toolbar. When this prop changes, the editor follows it. | | onNameChange | (name: string) => void | — | Called after a rename in the toolbar or an undo/redo that restores a name. | | className | string | — | Extra class on the editor root. | | style | CSSProperties | — | Inline style on the editor root. Use this to set height. | | page | number | — | Controlled page number, starting at 1. | | defaultPage | number | 1 | Initial page when page is not set. | | onPageChange | (page: number) => void | — | Called when the visible page changes, including from scrolling. | | scale | number | — | Controlled zoom. 1 is 100%. | | defaultScale | number | 1 | Initial zoom when scale is not set. | | minScale | number | 0.5 | Smallest zoom. | | maxScale | number | 3 | Largest zoom. | | onScaleChange | (scale: number) => void | — | Called when the zoom changes. | | showToolbar | boolean | true | false hides the toolbar even if toolbar is set. | | toolbar | (api: ToolbarApi) => ReactNode | Built-in toolbar | Render your own toolbar. Omit it to keep the default. | | showSidebar | boolean | true | false hides the pages sidebar even if sidebar is set. | | sidebar | (api: SidebarApi) => ReactNode | Built-in sidebar | Render your own pages sidebar. Omit it to keep the default. | | emptyState | ReactNode | "Open a PDF to get started." | Shown when file is empty. | | workerSrc | string | PDF.js worker on unpkg | URL of the PDF.js worker used to render pages. | | exportWorkerSrc | string | Bundled export worker | URL of a custom export worker. | | exportFileName | string | Document name | Download filename. .pdf is appended when missing. | | onExport | (blob: Blob) => void \| Promise<void> | Browser download | Receive the exported PDF yourself. When this is set, the editor does not download the file. | | onExportError | (error: Error) => void | — | Called when export fails. The toolbar also exposes exportError. | | onLoadSuccess | ({ numPages }) => void | — | Called after the PDF has loaded. | | onLoadError | (error: Error) => void | — | Called when the PDF cannot be loaded. |

PdfSource is string | URL | File | Blob | ArrayBuffer | Uint8Array.

Keep the page and zoom in your own state

Omit page and scale to let the editor own them. Pass them, and update them from the callbacks, to control them:

const [page, setPage] = useState(1);
const [scale, setScale] = useState(1);

<PdfEditor
  file={file}
  page={page}
  scale={scale}
  onPageChange={setPage}
  onScaleChange={setScale}
/>

Do the same for the document name if a parent must stay in sync:

const [name, setName] = useState("Contract");

<PdfEditor file={file} name={name} onNameChange={setName} />

Save the export yourself

<PdfEditor
  file={file}
  exportFileName="signed-contract.pdf"
  onExport={async (blob) => {
    const body = new FormData();
    body.append("file", blob, "signed-contract.pdf");
    await fetch("/api/documents", { method: "POST", body });
  }}
  onExportError={(error) => {
    console.error(error);
  }}
/>

onExport replaces the automatic download. Trigger a download yourself if you still want one:

onExport={(blob) => {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = "signed-contract.pdf";
  anchor.click();
  URL.revokeObjectURL(url);
}}

Custom toolbar

toolbar receives a ToolbarApi. showToolbar={false} removes the bar entirely. The custom toolbar demo is a full bar on this API: select, draw, highlight, text, image, and signature, plus undo, redo, zoom, pages, search, and export.

import { PdfEditor, Toolbar } from "react-web-pdf-editor";

<PdfEditor
  file={file}
  toolbar={(api) => (
    <header>
      <span>{api.name}</span>
      <button type="button" onClick={api.undo} disabled={!api.canUndo}>
        Undo
      </button>
      <button type="button" onClick={api.redo} disabled={!api.canRedo}>
        Redo
      </button>
      <button type="button" onClick={() => api.setTool("text")}>
        Text
      </button>
      <button type="button" onClick={api.zoomOut} disabled={api.scale <= api.minScale}>
        −
      </button>
      <span>{Math.round(api.scale * 100)}%</span>
      <button type="button" onClick={api.zoomIn} disabled={api.scale >= api.maxScale}>
        +
      </button>
      <span>
        {api.page} / {api.numPages}
      </span>
      <button type="button" onClick={api.exportPdf} disabled={api.disabled || api.exporting}>
        {api.exporting ? "Saving…" : "Save"}
      </button>
    </header>
  )}
/>

Render the exported Toolbar when you want the default bar inside your own layout:

<PdfEditor file={file} toolbar={(api) => <Toolbar {...api} />} />

ToolbarApi fields:

| Field | Description | | --- | --- | | name, setName | Document name. setName goes through the same rename path as the built-in field. | | disabled | true while no PDF is loaded. | | canUndo, canRedo, undo, redo | History. undoLabel and redoLabel describe the next step when present. | | shortcuts | Platform undo/redo labels: { undo, redo, undoKeys, redoKeys }. | | activeTool, setTool | "select", "draw", "text", "highlight", "image", or "signature". | | scale, minScale, maxScale, setScale, zoomIn, zoomOut | Zoom. | | page, numPages, setPage, prevPage, nextPage | Page navigation. | | search | { query, setQuery, matchCount, currentIndex, next, prev, clear }. currentIndex is -1 when there is no active match. | | exportPdf, exporting, exportError | Start an export, and read its pending or failed state. |

Custom sidebar

sidebar receives a SidebarApi. showSidebar={false} removes the sidebar entirely.

import { PdfEditor, PagesSidebar } from "react-web-pdf-editor";

<PdfEditor
  file={file}
  sidebar={(api) => (
    <aside>
      {api.pages.map((page) => (
        <button
          key={page.pageNumber}
          type="button"
          aria-current={page.selected ? "page" : undefined}
          onClick={() => api.setPage(page.pageNumber)}
        >
          Page {page.label}
        </button>
      ))}
      <button
        type="button"
        onClick={() => api.addBlankPage(api.page)}
        disabled={!api.canAdd || api.mutating}
      >
        Add page
      </button>
    </aside>
  )}
/>

Keep the default thumbnails by rendering PagesSidebar:

<PdfEditor file={file} sidebar={(api) => <PagesSidebar {...api} />} />

SidebarApi fields:

| Field | Description | | --- | --- | | pdf | The PDF.js document, or null while loading. | | page, numPages, setPage | Visible page and navigation. | | pages | Visual order. Each item has pageNumber, index, label, selected, and justAdded. | | addBlankPage(afterPage) | Insert a blank page after a 1-based page number. | | deletePage(pageNumber) | Remove a page. No effect when only one page remains. | | reorderPages(from, to) | Move a page. from and to are 1-based positions in the visual order. | | disabled | true while no PDF is loaded. | | mutating | true while a page insert, delete, or reorder is saving. | | adding | true while a blank page is being inserted. | | canAdd, canDelete, canReorder | Whether that page action is currently allowed. | | error | Message from the last failed page change, or null. | | drawStrokes, highlights, textAnnotations, signatures, images | Current annotations, used by the default thumbnails. |

pageNumber is the page's id in the loaded PDF. label is its position in the sidebar, which changes after a reorder.

Workers

PDF.js renders pages on a worker. The default is the legacy build, because that worker runs in its own thread and does not see polyfills on the page. The URL is a version-matched file on unpkg:

https://unpkg.com/pdfjs-dist@<version>/legacy/build/pdf.worker.min.mjs

Self-host that file and pass workerSrc when the CDN is blocked or you want the worker on your own origin. Copy it from node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs. A custom workerSrc has to be that legacy worker.

<PdfEditor file={file} workerSrc="/pdf.worker.min.mjs" />

You can also set it once, before the editor mounts:

import { configurePdfWorker } from "react-web-pdf-editor";

configurePdfWorker("/pdf.worker.min.mjs");

Export uses a second worker shipped with this package. Vite, Webpack, and other bundlers that understand new URL(..., import.meta.url) resolve it automatically. Pass exportWorkerSrc only if you need to host that worker at a specific URL. A failed worker start falls back to exporting on the main thread.

Next.js

Load the editor in the browser. The package talks to workers, canvas, and window during render. This is the basic demo. A direct import from a client component, without next/dynamic, is the quick start demo.

"use client";

import dynamic from "next/dynamic";
import "react-web-pdf-editor/styles.css";

const PdfEditor = dynamic(
  () => import("react-web-pdf-editor").then((mod) => mod.PdfEditor),
  { ssr: false },
);

export function DocumentEditor() {
  return <PdfEditor file="/contract.pdf" style={{ height: "100vh" }} />;
}

Styling

Import react-web-pdf-editor/styles.css once at the app root. Classes are prefixed with rpe-. Pass className and style to the root when you need to size or position the editor:

<PdfEditor
  file={file}
  className="contract-editor"
  style={{ height: "100vh", width: "100%" }}
/>

The editor fills the height you give it. Below 900px of editor width, that same layout uses the compact drawers described above. Desktop layout is unchanged above that width.

Exported types

These types are exported for custom toolbars, sidebars, and annotation handling:

PdfEditorProps, PdfSource, PdfLoadSuccess, Tool, ToolbarApi, ToolbarSearchState, ToolbarShortcuts, SidebarApi, SidebarPage, TextAnnotation, DrawStroke, DrawPoint, HighlightAnnotation, HighlightRect, ImageAnnotation, PendingImage, SignatureAnnotation, PendingSignature.

Local development

npm install
npm run dev

The playground runs at http://localhost:3000 and loads the library from src/. If port 3000 is already in use, the server does not start.

npm run build
npm run typecheck

npm run build writes ESM, CommonJS, and TypeScript declarations to dist/, and copies styles.css there. prepublishOnly runs that build before npm publish.

License

MIT