A framework-agnostic, universal document renderer with optional chunked loading polyrender.wisp.place/
TypeScript 83.9%
CSS 10.8%
JavaScript 2.7%
HTML 2.6%
17 1 0

Clone this repository

https://tangled.org/aria.pds.witchcraft.systems/polyrender https://tangled.org/did:plc:valun42etpm73we7bgyh64ge/polyrender
git@tangled.org:aria.pds.witchcraft.systems/polyrender git@tangled.org:did:plc:valun42etpm73we7bgyh64ge/polyrender

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

PolyRender#

A framework-agnostic, universal document renderer for the browser. Render PDFs, EPUBs, DOCX files, CSVs, source code, and plain text — with optional support for pre-rendered page images and chunked streaming for large documents.

Core (@polyrender/core) is a vanilla TypeScript library with zero framework dependencies. React (@polyrender/react) provides a thin wrapper component and hook. Both are designed for drop-in use in any web project.

Features#

  • Multi-format rendering — PDF, EPUB, DOCX, ODT, ODS, CSV/TSV, source code (100+ languages), plain text, and comic book archives (.cbz, .cbr, .cb7, .cbt)
  • Chunked loading — Stream large documents via pre-rendered page images or split PDF chunks
  • Fetch adapters — Pass data directly or provide a lazy-loading callback for on-demand fetching
  • CSS variable theming — Dark and light themes built in, fully customizable via --dv-* variables
  • Framework-agnostic — Use vanilla JS, React, or build your own wrapper
  • Lazy peer dependencies — Only loads renderer libraries (pdfjs, epubjs, etc.) when that format is actually used
  • Custom renderers — Register your own renderer for any format via the plugin registry
  • Word wrap / fit toggle — Toolbar button to toggle word wrap on code/text files and fit-to-width on comic pages
  • TypeScript-first — Complete type definitions for all APIs

Installation#

# Core (vanilla JS)
npm install @polyrender/core

# React wrapper
npm install @polyrender/react

# Install peer dependencies for the formats you need:
npm install pdfjs-dist       # PDF
npm install epubjs           # EPUB
npm install docx-preview     # DOCX
npm install papaparse        # CSV/TSV
npm install highlight.js     # Code syntax highlighting
npm install jszip            # ODT, CBZ comic archives
npm install xlsx             # ODS
npm install papaparse        # CSV/TSV
npm install highlight.js     # Code syntax highlighting

# Comic book archives — additional optional backends:
npm install node-unrar-js    # CBR (.cbr, RAR-compressed comics)
npm install 7z-wasm          # CB7 (.cb7, 7-Zip-compressed comics)

# Comic book archives — optional exotic image format decoders:
npm install @jsquash/jxl     # JPEG XL images inside archives
npm install utif             # TIFF images inside archives

You only need to install peer dependencies for the formats you plan to render. Unused formats won't add to your bundle.

Quick Start#

Vanilla JS#

import { PolyRender } from '@polyrender/core'
import '@polyrender/core/styles.css'

const viewer = new PolyRender(document.getElementById('viewer')!, {
  source: { type: 'url', url: '/document.pdf' },
  theme: 'dark',
  toolbar: true,
  onReady: (info) => {
    console.log(`Loaded: ${info.pageCount} pages`)
  },
  onPageChange: (page, total) => {
    console.log(`Page ${page} of ${total}`)
  },
})

// Imperative control
viewer.goToPage(5)
viewer.setZoom('fit-width')

// Clean up
viewer.destroy()

React#

import { DocumentViewer } from '@polyrender/react'
import '@polyrender/core/styles.css'

function App() {
  return (
    <DocumentViewer
      source={{ type: 'url', url: '/report.pdf' }}
      theme="dark"
      style={{ width: '100%', height: '80vh' }}
      onReady={(info) => console.log(`${info.pageCount} pages`)}
      onPageChange={(page, total) => console.log(`${page}/${total}`)}
    />
  )
}

React with Ref#

import { useRef } from 'react'
import { DocumentViewer, type DocumentViewerRef } from '@polyrender/react'
import '@polyrender/core/styles.css'

function App() {
  const viewerRef = useRef<DocumentViewerRef>(null)

  return (
    <>
      <DocumentViewer
        ref={viewerRef}
        source={{ type: 'url', url: '/report.pdf' }}
        style={{ width: '100%', height: '80vh' }}
      />
      <button onClick={() => viewerRef.current?.goToPage(1)}>
        Go to first page
      </button>
    </>
  )
}

React Hook (headless)#

import { useDocumentRenderer } from '@polyrender/react'
import '@polyrender/core/styles.css'

function CustomViewer({ url }: { url: string }) {
  const { containerRef, state, goToPage, setZoom } = useDocumentRenderer({
    source: { type: 'url', url },
    theme: 'dark',
    toolbar: false, // Hide built-in toolbar, build your own
  })

  return (
    <div>
      <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
      <div>
        <button onClick={() => goToPage(state.currentPage - 1)}>Prev</button>
        <span>{state.currentPage} / {state.totalPages}</span>
        <button onClick={() => goToPage(state.currentPage + 1)}>Next</button>
        <button onClick={() => setZoom(state.zoom * 1.2)}>Zoom In</button>
      </div>
    </div>
  )
}

Document Sources#

PolyRender accepts four types of document sources:

File (binary data)#

// From a File input
const file = inputElement.files[0]
source = { type: 'file', data: file, filename: file.name }

// From an ArrayBuffer
source = { type: 'file', data: arrayBuffer, mimeType: 'application/pdf' }

// From a Uint8Array
source = { type: 'file', data: uint8Array, filename: 'doc.pdf' }

URL#

source = { type: 'url', url: 'https://example.com/doc.pdf' }

// With custom headers (e.g., auth)
source = {
  type: 'url',
  url: '/api/documents/123.pdf',
  fetchOptions: { headers: { Authorization: 'Bearer ...' } },
}

Pre-rendered Pages (for browsing without the original document)#

// Direct data
source = {
  type: 'pages',
  pages: [
    { pageNumber: 1, imageUrl: '/pages/1.webp', width: 1654, height: 2339 },
    { pageNumber: 2, imageUrl: '/pages/2.webp', width: 1654, height: 2339 },
  ],
}

// Lazy fetch adapter (loads pages on demand as user scrolls)
source = {
  type: 'pages',
  pages: {
    totalPages: 500,
    fetchPage: async (pageNumber) => ({
      pageNumber,
      imageUrl: `/api/pages/${pageNumber}.webp`,
      width: 1654,
      height: 2339,
    }),
  },
}

Chunked PDF (streaming large documents)#

source = {
  type: 'chunked',
  totalPages: 500,
  // PDF chunks for full-fidelity rendering
  chunks: {
    totalChunks: 10,
    totalPages: 500,
    fetchChunk: async (index) => {
      const res = await fetch(`/api/chunks/${index}.pdf`)
      return {
        data: await res.arrayBuffer(),
        pageStart: index * 50 + 1,
        pageEnd: Math.min((index + 1) * 50, 500),
      }
    },
    getChunkIndexForPage: (page) => Math.floor((page - 1) / 50),
  },
  // Optional: fast browse images while chunks load
  browsePages: {
    totalPages: 500,
    fetchPage: async (pageNumber) => ({
      pageNumber,
      imageUrl: `/api/browse/${pageNumber}.webp`,
      width: 1654,
      height: 2339,
    }),
  },
}

Theming#

PolyRender uses CSS custom properties for all visual styling. Override any --dv-* variable to customize:

/* Custom theme */
.my-viewer .polyrender {
  --dv-bg: #1e1e2e;
  --dv-surface: #2a2a3e;
  --dv-text: #cdd6f4;
  --dv-accent: #89b4fa;
  --dv-border: #45475a;
  --dv-page-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  --dv-font-sans: 'JetBrains Mono', monospace;
}

Built-in themes: dark (default) and light. Set via the theme prop/option, or 'system' to auto-detect from prefers-color-scheme.

Key CSS Variables#

Variable Description
--dv-bg Background color
--dv-surface Toolbar and panel backgrounds
--dv-text Primary text color
--dv-text-secondary Secondary/muted text
--dv-accent Accent color (links, focus rings)
--dv-border Border color
--dv-page-bg Document page background
--dv-page-shadow Document page drop shadow
--dv-font-sans Sans-serif font stack
--dv-font-mono Monospace font stack
--dv-radius Border radius
--dv-toolbar-height Toolbar height

See styles.css for the complete list.

Format-Specific Options#

PDF#

{
  pdf: {
    workerSrc: '/pdf.worker.min.js', // pdf.js worker URL
    cMapUrl: '/cmaps/',               // Character map directory
    textLayer: true,                   // Enable text selection (default true)
    annotationLayer: false,            // Show PDF annotations
  }
}

Code#

{
  code: {
    language: 'typescript', // Force language (auto-detected from extension)
    lineNumbers: true,      // Show line numbers (default true)
    wordWrap: false,        // Enable word wrapping (default false)
    tabSize: 2,             // Tab width in spaces (default 2)
  }
}

CSV#

{
  csv: {
    delimiter: ',',    // Field delimiter (auto-detected)
    header: true,      // First row is header (default true)
    maxRows: 10000,    // Max rows to render (default 10000)
    sortable: true,    // Enable column sorting (default true)
  }
}

EPUB#

{
  epub: {
    flow: 'paginated', // 'paginated' or 'scrolled' (default 'paginated')
    fontSize: 16,       // Font size in pixels (default 16)
    fontFamily: 'Georgia', // Font override
  }
}

ODT#

{
  odt: {
    fontSize: 16,          // Base font size in pixels (default 16)
    fontFamily: 'Georgia', // Font override
  }
}

ODS#

{
  ods: {
    maxRows: 10000, // Max rows to render per sheet (default 10000)
    sortable: true, // Enable column sorting (default true)
    header: true,   // First row is header (default true)
  }
}

Custom Renderers#

Register a renderer for any format:

import { PolyRender, BaseRenderer, type PolyRenderOptions, type DocumentFormat } from '@polyrender/core'

class MarkdownRenderer extends BaseRenderer {
  readonly format: DocumentFormat = 'custom-markdown'

  protected async onMount(viewport: HTMLElement, options: PolyRenderOptions) {
    // Your rendering logic here
    const text = await this.loadText(options.source)
    const html = myMarkdownLib.render(text)
    viewport.innerHTML = html
    this.setReady({ format: 'custom-markdown', pageCount: 1 })
  }

  protected onDestroy() {}
}

// Register globally
PolyRender.registerRenderer('custom-markdown', () => new MarkdownRenderer())

// Use it
new PolyRender(container, {
  source: { type: 'url', url: '/readme.md' },
  format: 'custom-markdown',
})

Supported Formats#

Format Peer Dependency Auto-detected Extensions
PDF pdfjs-dist .pdf
EPUB epubjs .epub
DOCX docx-preview .docx, .doc
ODT jszip .odt
ODS xlsx .ods
CSV/TSV papaparse .csv, .tsv
Code highlight.js .js, .ts, .py, .rs, .go, .java, .c, .cpp, +80 more
Text (none) .txt
Markdown highlight.js .md (rendered as syntax-highlighted code)
JSON highlight.js .json
XML/HTML highlight.js .xml, .html, .svg
Pages (none) N/A (explicit type: 'pages')
Chunked PDF pdfjs-dist N/A (explicit type: 'chunked')
Comic — CBZ jszip .cbz
Comic — CBR node-unrar-js (optional) .cbr
Comic — CB7 7z-wasm (optional) .cb7
Comic — CBT (none, built-in TAR reader) .cbt
Comic — CBA ❌ not supported .cba

Browser Support#

  • Chrome/Edge 88+
  • Firefox 78+
  • Safari 15.4+ (OffscreenCanvas support for Web Worker rendering)

Project Structure#

packages/
├── core/           @polyrender/core — Framework-agnostic TypeScript core
│   ├── src/
│   │   ├── types.ts          # All interfaces and types
│   │   ├── polyrender.ts        # Main PolyRender class
│   │   ├── renderer.ts       # Abstract base renderer
│   │   ├── registry.ts       # Format → renderer factory mapping
│   │   ├── toolbar.ts        # Built-in toolbar DOM builder
│   │   ├── utils.ts          # Format detection, data conversion, DOM helpers
│   │   ├── styles.css        # CSS variables theme system
│   │   └── renderers/
│   │       ├── pdf.ts        # PDF (pdfjs-dist)
│   │       ├── browse-pages.ts # Pre-rendered page images
│   │       ├── chunked-pdf.ts  # Chunked PDF streaming
│   │       ├── epub.ts       # EPUB (epubjs)
│   │       ├── docx.ts       # DOCX (docx-preview)
│   │       ├── odt.ts        # ODT (jszip)
│   │       ├── ods.ts        # ODS (xlsx)
│   │       ├── csv.ts        # CSV/TSV (papaparse)
│   │       ├── code.ts       # Code (highlight.js)
│   │       ├── text.ts       # Plain text
│   │       └── comic.ts      # Comic book archives (jszip / node-unrar-js / 7z-wasm)
│   └── package.json
└── react/          @polyrender/react — React wrapper
    ├── src/
    │   ├── DocumentViewer.tsx       # Drop-in component
    │   ├── useDocumentRenderer.ts   # Headless hook
    │   └── index.ts
    └── package.json

Live Demo#

A hosted version of the vanilla example is available at https://polyrender.wisp.place/.

Repository#

The source code is hosted in two locations:

License#

Zlib