PDF Viewer Implementation for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1262
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    874
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    851

Implementing PDF Viewer on Website

An embedded PDF viewer is needed in document management systems, contract pages, regulatory document portals, reporting systems. <iframe src="file.pdf" title="Embedded content"> is not an option: behavior depends on browser, mobile Chrome downloads file instead of showing, no UI control.

PDF.js

Mozilla PDF.js — standard for browser PDF rendering. Uses Canvas to render each page. Firefox uses it for built-in viewer.

npm install pdfjs-dist
npm install react-pdf  # React wrapper over PDF.js

react-pdf: Simple Implementation

import { Document, Page, pdfjs } from 'react-pdf'
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'

// Must specify worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString()

interface PDFViewerProps {
  url: string
}

export function PDFViewer({ url }: PDFViewerProps) {
  const [numPages, setNumPages] = useState<number>(0)
  const [pageNumber, setPageNumber] = useState<number>(1)
  const [scale, setScale] = useState<number>(1.0)
  const [isLoading, setIsLoading] = useState(true)

  function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
    setNumPages(numPages)
    setIsLoading(false)
  }

  return (
    <div className="flex flex-col items-center">
      {/* Toolbar */}
      <div className="flex items-center gap-4 p-3 bg-gray-800 text-white w-full">
        <button
          onClick={() => setPageNumber((p) => Math.max(1, p - 1))}
          disabled={pageNumber <= 1}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          ←
        </button>
        <span className="text-sm">
          {pageNumber} / {numPages}
        </span>
        <button
          onClick={() => setPageNumber((p) => Math.min(numPages, p + 1))}
          disabled={pageNumber >= numPages}
          className="px-3 py-1 bg-gray-600 rounded disabled:opacity-40"
        >
          →
        </button>
        <div className="ml-auto flex items-center gap-2">
          <button onClick={() => setScale((s) => Math.max(0.5, s - 0.25))}>−</button>
          <span className="text-sm w-12 text-center">{Math.round(scale * 100)}%</span>
          <button onClick={() => setScale((s) => Math.min(3, s + 0.25))}>+</button>
        </div>
      </div>

      {/* Document */}
      <div className="overflow-auto bg-gray-200 w-full" style={{ maxHeight: '80vh' }}>
        <Document
          file={url}
          onLoadSuccess={onDocumentLoadSuccess}
          loading={<div className="p-8 text-center">Loading...</div>}
          error={<div className="p-8 text-center text-red-500">Error loading PDF</div>}
        >
          <Page
            pageNumber={pageNumber}
            scale={scale}
            renderTextLayer={true}     // Selectable text
            renderAnnotationLayer={true} // Clickable links
            className="shadow-lg mx-auto my-4"
          />
        </Document>
      </div>
    </div>
  )
}

Show All Pages (scroll mode)

For long documents, scrolling is more convenient than pagination:

function PDFScrollViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [containerWidth, setContainerWidth] = useState(0)
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!containerRef.current) return
    const observer = new ResizeObserver(([entry]) => {
      setContainerWidth(entry.contentRect.width)
    })
    observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [])

  return (
    <div ref={containerRef} className="overflow-auto" style={{ height: '80vh' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        {Array.from({ length: numPages }, (_, i) => (
          <Page
            key={i + 1}
            pageNumber={i + 1}
            width={containerWidth - 32}  // Responsive width
            className="mb-4 shadow mx-4"
            renderTextLayer={true}
          />
        ))}
      </Document>
    </div>
  )
}

Virtualizing Pages for Large PDFs

Don't render 100-page PDF all at once. Use virtualization:

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualPDFViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState(0)
  const [pageHeight] = useState(842) // A4 at scale=1
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: numPages,
    getScrollElement: () => parentRef.current,
    estimateSize: () => pageHeight + 16, // page height + gap
    overscan: 2,
  })

  return (
    <div ref={parentRef} style={{ height: '80vh', overflow: 'auto' }}>
      <Document file={url} onLoadSuccess={({ numPages }) => setNumPages(numPages)}>
        <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
          {virtualizer.getVirtualItems().map((virtualItem) => (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                transform: `translateY(${virtualItem.start}px)`,
                width: '100%',
                padding: '8px 16px',
              }}
            >
              <Page
                pageNumber={virtualItem.index + 1}
                width={600}
                renderTextLayer={false}  // Disable for speed
              />
            </div>
          ))}
        </div>
      </Document>
    </div>
  )
}

Protected PDF via Blob URL

So user can't download file directly via URL:

async function loadProtectedPDF(documentId: string): Promise<string> {
  const response = await fetch(`/api/documents/${documentId}/content`, {
    headers: { Authorization: `Bearer ${getToken()}` },
  })

  if (!response.ok) throw new Error('Access denied')

  const blob = await response.blob()
  return URL.createObjectURL(blob)
  // Blob URL valid only within browser session
  // No direct link — only through API with auth
}

What We Do

Set up PDF.js with react-pdf, choose display mode (paginated or scroll), add toolbar with zoom and navigation. For large documents — virtualize pages. For closed documents — load through authorized API with Blob URL.

Timeframe: basic viewer — 1 day. With virtualization and access protection — 2–3 days.