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

pdf-tsx

v0.15.1

Published

A fully-featured, composable PDF viewer component for React

Readme

pdf-tsx

A fully-featured, composable PDF viewer component for React.

Installation

npm install pdf-tsx

Peer dependencies

npm install react react-dom pdfjs-dist

CSS

Import the stylesheet once in your app entry point:

import 'pdf-tsx/dist/es/pdf-tsx.css'

Quick start

PDFViewer requires a workerSrc prop — the URL of the pdfjs worker script. The easiest way is to serve it from the pdfjs-dist package directly:

import { PDFViewer } from 'pdf-tsx'
import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'

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

  if (!file) return <input type="file" accept=".pdf" onChange={e => setFile(e.target.files![0])} />

  return <PDFViewer file={file} workerSrc={workerUrl} />
}

The ?url suffix is a Vite feature that returns the asset URL as a string. If you use a different bundler, import the worker URL accordingly or pass a CDN URL:

workerSrc="https://unpkg.com/pdfjs-dist@5/build/pdf.worker.min.mjs"

Icons

Feature components do not bundle any icon library. Each component that shows an icon requires you to pass it explicitly as a prop — use any icon library or custom SVG you prefer.

import { LuZoomIn, LuZoomOut } from 'react-icons/lu'

<PDFZoomControls zoomInIcon={<LuZoomIn size={16} />} zoomOutIcon={<LuZoomOut size={16} />} />

PDFViewer

The main component. Must wrap all feature components.

<PDFViewer
  file={file}
  workerSrc={workerUrl}
  sidebar={<PDFSidebar panels={[...]} />}
  toolbar={<>...</>}
  onChangeFile={(newFile) => setFile(newFile)}
/>

| Prop | Type | Required | Description | |------------------|---------------------------------------------------------|----------|------------------------------------------------------| | file | File | yes | The PDF file object to display | | workerSrc | string | yes | URL of the pdfjs worker script (see Quick start) | | sidebar | ReactNode | no | Content rendered on the left side | | toolbar | ReactNode | no | Content rendered in the top toolbar | | onChangeFile | (file: File) => void | no | Called when the user opens a different file | | defaultTheme | "light" \| "dark" | no | Initial theme mode (default: "light") | | themeOverrides | { light?: Partial<Theme>; dark?: Partial<Theme> } | no | Override individual theme tokens (see Theming) | | defaultFit | "width" \| "page" | no | Initial zoom mode when a PDF is loaded | | language | "it" \| "en" | no | UI language (default: "en") | | className | string | no | CSS class applied to the root layout element | | style | React.CSSProperties | no | Inline style applied to the root layout element | | requireRead | boolean | no | Show a mandatory-reading banner (see Mandatory reading) | | onDocumentRead | () => void | no | Called once when the user reaches the last page |


Theming

PDFViewer ships with built-in light and dark themes. The default is "light". Use defaultTheme to set the initial mode and themeOverrides to override individual tokens without replacing the whole theme.

import { PDFViewer, darkTheme } from 'pdf-tsx'

<PDFViewer
  themeOverrides={{
    dark:  { accent: '#e11d48', accentHover: '#be123c', accentDark: '#9f1239' },
    light: { accent: '#e11d48', accentHover: '#be123c', accentDark: '#9f1239' },
  }}
  ...
/>

Both darkTheme and lightTheme are exported so you can spread them as a base:

import { darkTheme } from 'pdf-tsx'

themeOverrides={{ dark: { ...darkTheme, bg: '#0f0f0f' } }}

Theme tokens (Theme interface):

| Token | CSS variable | Description | |--------------------|-----------------------------|--------------------------------------| | bg | --pdf-bg | Main background | | surface | --pdf-surface | Card / panel surface | | surfaceHover | --pdf-surface-hover | Surface hover state | | border | --pdf-border | Default border color | | borderLight | --pdf-border-light | Subtle border color | | scrollArea | --pdf-scroll-area | Scroll container background | | textPrimary | --pdf-text-primary | Primary text | | textSecondary | --pdf-text-secondary | Secondary text | | textMuted | --pdf-text-muted | Muted / disabled text | | textPlaceholder | --pdf-text-placeholder | Input placeholder text | | accent | --pdf-accent | Accent / interactive color | | accentHover | --pdf-accent-hover | Accent hover state | | accentDark | --pdf-accent-dark | Accent dark variant | | accentLight | --pdf-accent-light | Accent light variant | | accentSubtle | --pdf-accent-subtle | Accent with low opacity (backgrounds)| | panelBg | --pdf-panel-bg | Sidebar panel background | | panelBorder | --pdf-panel-border | Sidebar panel border | | panelHeaderBg | --pdf-panel-header-bg | Sidebar panel header background | | shadowPage | --pdf-shadow-page | Drop shadow color for PDF pages | | badgeBg | --pdf-badge-bg | Count badge background (signatures, annotations, attachments) | | toolbarSeparator | --pdf-toolbar-separator | PDFToolbarSeparator line color | | fontFamily | --pdf-font-family | Font family for all viewer UI text |


PDFSidebar

A collapsible icon sidebar. Each panel has an icon button — clicking it toggles the panel open/closed. When collapsed via PDFSidebarToggle, the entire sidebar is hidden (null).

import { PDFSidebar } from 'pdf-tsx'
import { LuLayoutGrid, LuBookOpen } from 'react-icons/lu'

<PDFSidebar
  panels={[
    { icon: <LuLayoutGrid size={18} />, label: 'Thumbnails', content: <PDFThumbnails /> },
    { icon: <LuBookOpen size={18} />,   label: 'Outline',    content: <PDFOutline /> },
  ]}
/>

| Prop | Type | Description | |----------|------------------|------------------------------| | panels | SidebarPanel[] | Array of panels to display |

SidebarPanel

| Field | Type | Description | |-----------|-------------|----------------------------------------| | icon | ReactNode | Icon shown in the sidebar button bar | | label | string | Panel title shown in the header | | content | ReactNode | Content rendered inside the open panel |

Sidebar hide/show pattern

Use PDFSidebarToggle in the toolbar alongside PDFSidebar. When the sidebar is open, the collapse button lives inside the sidebar. When collapsed, the expand button appears in the toolbar.

import { PDFSidebar, PDFSidebarToggle, PDFToolbar } from 'pdf-tsx'
import { LuPanelLeftClose, LuPanelLeftOpen, LuEllipsis } from 'react-icons/lu'

<PDFViewer
  sidebar={
    <PDFSidebar panels={[...]} />
  }
  toolbar={
    <PDFToolbar overflowIcon={<LuEllipsis size={16} />}>
      <PDFSidebarToggle
        collapseIcon={<LuPanelLeftClose size={16} />}
        expandIcon={<LuPanelLeftOpen size={16} />}
      />
      {/* other toolbar items */}
    </PDFToolbar>
  }
/>

PDFToolbar (responsive toolbar)

PDFToolbar wraps your toolbar items and automatically collapses those that don't fit the available width into an overflow ··· dropdown. It uses ResizeObserver to react to container size changes — no configuration required.

import { PDFToolbar } from 'pdf-tsx'
import { LuEllipsis, LuPanelLeftClose, LuPanelLeftOpen, LuSearch, LuChevronUp, LuChevronDown, LuX, LuZoomOut, LuZoomIn, LuChevronLeft, LuChevronRight } from 'react-icons/lu'

<PDFViewer
  toolbar={
    <PDFToolbar overflowIcon={<LuEllipsis size={16} />}>
      <PDFSidebarToggle collapseIcon={<LuPanelLeftClose size={16} />} expandIcon={<LuPanelLeftOpen size={16} />} />
      <PDFSearch searchIcon={<LuSearch size={14} />} prevIcon={<LuChevronUp size={14} />} nextIcon={<LuChevronDown size={14} />} clearIcon={<LuX size={14} />} />
      <PDFZoomControls zoomOutIcon={<LuZoomOut size={16} />} zoomInIcon={<LuZoomIn size={16} />} />
      <PDFNavigation prevIcon={<LuChevronLeft size={16} />} nextIcon={<LuChevronRight size={16} />} />
      {/* … more items */}
    </PDFToolbar>
  }
/>

| Prop | Type | Description | |----------------|-------------|-------------------------------------------------------| | children | ReactNode | Toolbar items in priority order (left = highest) | | overflowIcon | ReactNode | Icon for the ··· overflow button (default: "···") |

Dropdown labels — all built-in feature components carry a toolbarLabelKey static property. When an item is moved to the overflow dropdown, PDFToolbar resolves the label from the active language automatically. No extra configuration needed.

Custom items — wrap any custom component in PDFToolbarItem to give it a dropdown label:

import { PDFToolbarItem } from 'pdf-tsx'

<PDFToolbar overflowIcon={<LuEllipsis size={16} />}>
  <PDFToolbarItem label="Custom">
    <MyCustomButton />
  </PDFToolbarItem>
</PDFToolbar>

Feature components

All feature components must be rendered inside <PDFViewer>. They read and write state via usePDFViewer() internally.

| Component | Icon props | Description | |------------------------|-----------------------------------------------------|------------------------------------------------------| | PDFToolbar | overflowIcon | Responsive toolbar — collapses items into ··· dropdown when space runs out | | PDFToolbarItem | — | Wrapper for custom toolbar items to add a dropdown label | | PDFToolbarSeparator | — | Vertical divider between toolbar groups — hidden automatically in the overflow dropdown | | PDFThumbnails | — | Page thumbnail strip for quick navigation | | PDFOutline | — | Document outline / bookmarks tree | | PDFAnnotations | — | Display PDF annotations | | PDFAnnotationsIcon | icon | Sidebar icon with annotation count badge | | PDFSignatures | — | Display PDF digital signatures | | PDFSignaturesIcon | icon | Sidebar icon with signature count badge | | PDFAttachments | — | List embedded file attachments with download buttons | | PDFAttachmentsIcon | icon | Sidebar icon with attachment count badge | | PDFSearch | searchIcon prevIcon nextIcon clearIcon | Full-text search with match highlighting. Accepts optional defaultSearch to pre-fill and auto-navigate | | PDFZoomControls | zoomInIcon zoomOutIcon | Zoom in / zoom out buttons with percentage display | | PDFPageFit | fitWidthIcon fitPageIcon | Fit-to-width and fit-to-page buttons | | PDFRotationControl | icon | Rotate page clockwise | | PDFNavigation | prevIcon nextIcon | Previous / next page buttons with current page input | | PDFDownloadButton | icon | Download the current PDF file | | PDFChangeFile | icon | Button to open a new PDF file | | PDFThemeToggle | lightIcon darkIcon | Toggle between dark and light mode | | PDFPrintButton | icon | Print the PDF using the browser's native print dialog| | PDFSidebarToggle | collapseIcon expandIcon | Toolbar button to expand the sidebar — visible only when sidebar is hidden (see Sidebar hide/show pattern) |

PDFSearch

| Prop | Type | Required | Description | |----------------|-------------|----------|-----------------------------------------------------------------------------| | searchIcon | ReactNode | yes | Icon shown inside the search input | | prevIcon | ReactNode | yes | Icon for the "previous match" button | | nextIcon | ReactNode | yes | Icon for the "next match" button | | clearIcon | ReactNode | yes | Icon for the "clear search" button | | initialSearch| string | no | Pre-fills the search input and automatically navigates to the first match once the PDF is loaded. Navigation fires once — subsequent manual edits are unaffected. |

<PDFSearch
  initialSearch="invoice"
  searchIcon={<LuSearch size={14} />}
  prevIcon={<LuChevronUp size={14} />}
  nextIcon={<LuChevronDown size={14} />}
  clearIcon={<LuX size={14} />}
/>

Count badges

PDFSignaturesIcon, PDFAnnotationsIcon, and PDFAttachmentsIcon are icon wrapper components intended for use as the icon prop in PDFSidebar panels. They render the provided icon with a small count badge in the top-right corner, automatically populated from the loaded PDF.

import {
  PDFSidebar,
  PDFSignatures, PDFSignaturesIcon,
  PDFAnnotations, PDFAnnotationsIcon,
  PDFAttachments, PDFAttachmentsIcon,
} from 'pdf-tsx'
import { LuSignature, LuStickyNote, LuPaperclip } from 'react-icons/lu'

<PDFSidebar panels={[
  {
    icon: <PDFSignaturesIcon icon={<LuSignature size={17} />} />,
    label: 'Signatures',
    content: <PDFSignatures />,
  },
  {
    icon: <PDFAnnotationsIcon icon={<LuStickyNote size={17} />} />,
    label: 'Annotations',
    content: <PDFAnnotations />,
  },
  {
    icon: <PDFAttachmentsIcon icon={<LuPaperclip size={17} />} />,
    label: 'Attachments',
    content: <PDFAttachments />,
  },
]} />

The badge uses var(--pdf-accent) and respects themeOverrides.


Mandatory reading

Use requireRead when the user must scroll through the entire document before proceeding (e.g. terms and conditions, consent forms).

const [canProceed, setCanProceed] = useState(false)

<PDFViewer
  requireRead
  onDocumentRead={() => setCanProceed(true)}
  ...
/>

<button disabled={!canProceed}>Accetto</button>

PDFViewer renders a sticky banner at the bottom of the scroll area:

  • Pending — accent color, arrow-down icon, label requireReadPending ("Scroll to the end to continue")
  • Confirmed — green (#16a34a), checkmark icon, label requireReadDone ("Document read") — the banner stays visible after confirmation

onDocumentRead fires at most once per loaded file. Both banner labels are part of the Labels interface and are translated automatically when language="it" is set.


Localisation

PDFViewer ships with English (default) and Italian label sets. Switch via the language prop:

<PDFViewer language="it" ... />

Both label objects are exported if you need to inspect them:

import { italianLabels, englishLabels } from 'pdf-tsx'

The Labels interface (also exported) describes every string key. The built-in toolbar overflow labels are resolved automatically from the active language — no extra configuration needed.


Full example

import { useState, Suspense, lazy } from 'react'
import {
  PDFSidebar, PDFSidebarToggle, PDFToolbar, PDFToolbarItem, PDFToolbarSeparator,
  PDFThumbnails, PDFOutline, PDFSearch,
  PDFZoomControls, PDFPageFit, PDFRotationControl,
  PDFNavigation, PDFDownloadButton, PDFChangeFile,
  PDFThemeToggle, PDFPrintButton,
  PDFSignatures, PDFSignaturesIcon,
  PDFAnnotations, PDFAnnotationsIcon,
  PDFAttachments, PDFAttachmentsIcon,
} from 'pdf-tsx'
import {
  LuLayoutGrid, LuBookOpen, LuSignature, LuStickyNote, LuPaperclip,
  LuSearch, LuChevronUp, LuChevronDown, LuX,
  LuZoomIn, LuZoomOut, LuArrowLeftRight, LuScan, LuRotateCw,
  LuChevronLeft, LuChevronRight, LuDownload, LuFolderPlus,
  LuPrinter, LuSun, LuMoon, LuPanelLeftClose, LuPanelLeftOpen, LuEllipsis,
} from 'react-icons/lu'
import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'

const PDFViewer = lazy(() => import('pdf-tsx').then(m => ({ default: m.PDFViewer })))

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

  if (!file) {
    return (
      <input
        type="file"
        accept=".pdf"
        onChange={e => setFile(e.target.files![0])}
      />
    )
  }

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <PDFViewer
        file={file}
        workerSrc={workerUrl}
        defaultFit="width"
        onChangeFile={setFile}
        sidebar={
          <PDFSidebar
            panels={[
              { icon: <LuLayoutGrid size={18} />,                                    label: 'Thumbnails',  content: <PDFThumbnails /> },
              { icon: <LuBookOpen size={18} />,                                      label: 'Outline',     content: <PDFOutline /> },
              { icon: <PDFSignaturesIcon icon={<LuSignature size={17} />} />,        label: 'Signatures',  content: <PDFSignatures /> },
              { icon: <PDFAnnotationsIcon icon={<LuStickyNote size={17} />} />,      label: 'Annotations', content: <PDFAnnotations /> },
              { icon: <PDFAttachmentsIcon icon={<LuPaperclip size={17} />} />,       label: 'Attachments', content: <PDFAttachments /> },
            ]}
          />
        }
        toolbar={
          <PDFToolbar overflowIcon={<LuEllipsis size={16} />}>
            <PDFSidebarToggle
              collapseIcon={<LuPanelLeftClose size={16} />}
              expandIcon={<LuPanelLeftOpen size={16} />}
            />
            <PDFToolbarSeparator />
            <PDFSearch
              searchIcon={<LuSearch size={14} />}
              prevIcon={<LuChevronUp size={14} />}
              nextIcon={<LuChevronDown size={14} />}
              clearIcon={<LuX size={14} />}
            />
            <PDFToolbarSeparator />
            <PDFZoomControls zoomOutIcon={<LuZoomOut size={16} />} zoomInIcon={<LuZoomIn size={16} />} />
            <PDFRotationControl icon={<LuRotateCw size={16} />} />
            <PDFPageFit fitWidthIcon={<LuArrowLeftRight size={16} />} fitPageIcon={<LuScan size={16} />} />
            <PDFToolbarSeparator />
            <PDFNavigation prevIcon={<LuChevronLeft size={16} />} nextIcon={<LuChevronRight size={16} />} />
            <PDFToolbarSeparator />
            <PDFThemeToggle lightIcon={<LuSun size={16} />} darkIcon={<LuMoon size={16} />} />
            <PDFPrintButton icon={<LuPrinter size={16} />} />
            <PDFChangeFile icon={<LuFolderPlus size={16} />} />
            <PDFDownloadButton icon={<LuDownload size={15} />} />
            {/* custom button with overflow label */}
            <PDFToolbarItem label="Share">
              <button onClick={() => {}}>Share</button>
            </PDFToolbarItem>
          </PDFToolbar>
        }
      />
    </Suspense>
  )
}

Keyboard shortcuts

When focus is not on an input or textarea, the following keys are active:

| Key | Action | |-----------------------------|------------------| | ArrowLeft / ArrowUp | Previous page | | ArrowRight / ArrowDown | Next page | | r / R | Rotate +90° |


Performance

pdf-tsx uses virtual rendering: only the pages near the current view are rendered at any one time. The default window is the current page ±2. Pages with an active highlight or search match are always included regardless of position.

This means:

  • Initial load renders at most 5 pages, regardless of document length
  • Memory and CPU usage stay flat as the user scrolls through a large document
  • Pages that have already been rendered are kept in the DOM as the user scrolls, avoiding redundant re-renders

Render cascades are minimised via manual memoization: the context value is wrapped in useMemo so a zoom or page change does not re-render unrelated consumers, and inner sub-components (AnnotationCard, SignatureCard, OutlineItem) are wrapped in React.memo.

No configuration is needed — this is the default behaviour.


License

MIT © Riccardo Grossano — see LICENSE for full terms.