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

@joelouf/doc-viewer

v1.2.0

Published

A modular, zero-dependency document page viewer built for displaying pre-rendered page images in a print-preview-style interface.

Readme

@joelouf/doc-viewer

A modular, zero-dependency document page viewer built for displaying pre-rendered page images in a print-preview-style interface. The core logic ships as framework-agnostic modules consumed by both a Web Component and React hooks, including:

  • Zoom math
  • Image loading
  • Virtualization
  • Reduced-motion detection

This component is strictly a display layer. All document parsing happens server-side. You provide it an array of image URLs (one per page), and it handles the rest.

Features

  • Zero dependencies - no runtime dependencies for the core or Web Component; React is an optional peer dependency for the hooks layer
  • Web Component + React hooks - use the self-contained <document-page-viewer> custom element in any framework, or compose your own UI with the React hooks
  • Modular architecture - core modules (zoom, image loading, virtualization, reduced motion) are independently importable and useful outside the viewer
  • Shadow DOM encapsulation - styles are fully isolated and won't leak into or be affected by your application's CSS
  • CSS custom property theming - override a handful of CSS variables to match your app's design system
  • Keyboard navigation - full keyboard support for page navigation, zoom, fullscreen, and sidebar toggling
  • Thumbnail sidebar - collapsible sidebar with page thumbnails for quick navigation
  • Fit modes - fit to width, fit to page, and actual size (100%), with discrete zoom steps from 25% to 400%
  • Scroll-center-preserving zoom - zooming in/out keeps the viewport centered on the same content
  • Image preloading - adjacent pages are preloaded ahead of time for seamless scrolling
  • Page virtualization - pages far from the current viewport are unloaded to conserve memory in large documents
  • Pinch-to-zoom - native two-finger zoom on touch devices
  • Ctrl+Wheel zoom - standard desktop zoom gesture
  • Click-and-drag panning - grab and pan when content overflows the viewport
  • Fullscreen support - toggle fullscreen with a button or the F key
  • Reduced motion support - detects prefers-reduced-motion at runtime (not just via CSS) and disables smooth scrolling and animations accordingly
  • Error handling and retry - failed page loads show an error state with a retry button
  • Loading skeletons - shimmer placeholders while pages load
  • Mobile responsive - adaptive toolbar with collapsible zoom controls on small screens
  • Accessible - ARIA roles, labels, live regions, and screen reader announcements throughout

Architecture

core/
  types.js                  # Constants, URL validation, fit-mode checks
  zoom.js                   # clampZoom, nextZoomStep, computeFitZoom, etc.
  image-loader.js           # ImageLoadController (load, preload, retry, virtualize)
  reduced-motion.js         # ReducedMotionController (live media query listener)
---
document-page-viewer.js     # Web Component (Shadow DOM, toolbar, thumbnails, observers)
---
react/
  useZoom.js                # Hook wrapping core zoom functions
  usePageVirtualization.js  # Hook wrapping ImageLoadController
  useDocumentViewer.js      # Composed hook (zoom + virtualization + keyboard + fullscreen)

Each layer is independently useful. useZoom can power any zoomable interface. usePageVirtualization can manage image loading for any scroll-based gallery. The Web Component composes them all into a self-contained viewer.

Install

npm install @joelouf/doc-viewer

Quick Start

Plain HTML (Web Component)

<script type="module" src="@joelouf/doc-viewer/document-page-viewer.js"></script>
 
<document-page-viewer
  id="viewer"
  show-thumbnails
  fit-mode="width"
  style="width: 100%; height: 100vh;">
</document-page-viewer>
 
<script>
  document.getElementById('viewer').pages = [
    '/pages/page-1.png',
    '/pages/page-2.png',
    '/pages/page-3.png'
  ];
</script>

React (Hooks)

import { useDocumentViewer } from '@joelouf/doc-viewer/react';
 
function Viewer({ pages }) {
  const viewer = useDocumentViewer({ pages, defaultFitMode: 'width' });
 
  // Build your own UI using:
  // viewer.zoom, viewer.currentPage, viewer.handleKeyDown, viewer.zoomIn(), viewer.zoomOut(), viewer.scrollToPage(n), etc.
}

Vue / Svelte / Angular

The Web Component works in any framework:

<script type="module">
  import '@joelouf/doc-viewer';
</script>
 
<document-page-viewer show-thumbnails fit-mode="width"></document-page-viewer>

Core Only

Import the framework-agnostic modules directly:

import {
  computeFitZoom,
  clampZoom,
  ImageLoadController
} from '@joelouf/doc-viewer/core';
 
const zoom = computeFitZoom('width', { w: 800, h: 600 });

Blob Input (Dynamically Generated Pages)

Pass Blob objects directly as page sources. The viewer manages object URL creation and cleanup internally.

// Example: canvas-rendered pages passed as Blobs.
const blobs = await Promise.all(
  canvases.map((canvas) =>
    new Promise((resolve) => canvas.toBlob(resolve, 'image/png'))
  )
);

document.getElementById('viewer').pages = blobs;
// React: mixed URL and Blob sources.
const viewer = useDocumentViewer({
  pages: ['/static/cover.png', dynamicBlob, '/static/back.png'],
});

API

Web Component Attributes

| Attribute | Property | Type | Default | |---|---|---|---| | pages | pages | (string \| Blob)[] | [] | | default-zoom | defaultZoom | number | 1.0 | | fit-mode | fitMode | 'width' \| 'page' \| 'actual' | 'width' | | show-thumbnails | showThumbnails | boolean | true | | class-name | - | string | - |

The pages attribute accepts a JSON-stringified array of image URLs. The pages property accepts a native JavaScript array of URL strings, Blob objects, or a mix of both.

Read-Only Properties

| Property | Type | Description | |---|---|---| | currentPage | number | Currently visible page (1-based) | | zoom | number | Current zoom level (0.25-4.0) | | activeFitMode | string \| null | Active fit mode, or null if manually zoomed | | prefersReducedMotion | boolean | Live reduced-motion preference |

Public Methods

| Method | Description | |---|---| | scrollToPage(num) | Scroll to a specific page (1-based) | | setZoom(level) | Set zoom to a specific level (clamped to 0.25-4.0) |

Events

| Event | detail | |---|---| | page-change | { page, previousPage, total } | | zoom-change | { zoom, previousZoom, percent, fitMode } |

const viewer = document.querySelector('document-page-viewer');
 
viewer.addEventListener('page-change', (e) => {
  console.log(`Page ${e.detail.page} of ${e.detail.total}`);
});
 
viewer.addEventListener('zoom-change', (e) => {
  console.log(`Zoom: ${e.detail.percent}`);
});

Keyboard Shortcuts

| Key | Action | |---|---| | / PageUp | Previous page | | / PageDown | Next page | | Home / End | First / last page | | + / Ctrl+= | Zoom in | | - / Ctrl+- | Zoom out | | F | Toggle fullscreen | | T | Toggle thumbnails | | Escape | Exit fullscreen |

React Hooks

useDocumentViewer(options?)

The primary hook. Composes zoom, virtualization, keyboard shortcuts, fullscreen, and reduced motion into a single return object.

const viewer = useDocumentViewer({
  pages: ['/page-1.png', '/page-2.png'],
  // 'width' | 'page' | 'actual'
  defaultFitMode: 'width',
  defaultZoom: 1.0,
  showThumbnails: true,
  onPageChange: (detail) => {},
  onZoomChange: (detail) => {},
});

Returns state (currentPage, zoom, fitMode, isFullscreen, prefersReducedMotion, totalPages), actions (zoomIn, zoomOut, setZoom, applyFitMode, scrollToPage, toggleFullscreen, toggleThumbnails), virtualization methods (loadPage, loadWithNeighbors, retryPage, virtualizePage, getPageState, getNaturalSize), event handlers (handleKeyDown), and refs (viewportRef, hostRef).

useZoom(options?)

Standalone zoom state management. Useful for building any zoomable interface.

const { zoom, fitMode, zoomIn, zoomOut, setZoom, applyFitMode, zoomPercent } = useZoom({
  defaultZoom: 1.0,
  defaultFitMode: 'width',
  onZoomChange: (detail) => {},
});

usePageVirtualization({ pages })

Standalone image loading and virtualization. Useful for any scroll-based image gallery.

const {
  imageLoader, loadGeneration, getPageState, getNaturalSize,
  getFirstLoadedAspect, loadPage, loadWithNeighbors, retryPage, virtualizePage,
} = usePageVirtualization({ pages });

Core Modules

All core modules are framework-agnostic, pure functions or imperative controllers with zero DOM side effects (except ImageLoadController which creates <img> elements and ReducedMotionController which listens to a media query).

Zoom Functions

import {
  // Clamp and snap to nearest step
  clampZoom,
  // Next discrete zoom level up
  nextZoomStep,
  // Next discrete zoom level down
  prevZoomStep,
  // Calculate zoom for a fit mode + viewport
  computeFitZoom,
  // Page display width at a zoom level
  computePageDisplaySize,
  // Scroll position adjustment after zoom change
  preserveScrollCenter,
} from '@joelouf/doc-viewer/core';

Constants

import {
  // ['width', 'page', 'actual']
  FIT_MODES,
  // [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0]
  ZOOM_STEPS,
  // 8.5 / 11 (US Letter)
  DEFAULT_PAGE_ASPECT,
  // 816 (96dpi × 8.5 inches)
  PAGE_NATURAL_WIDTH,
  // 24px between pages
  PAGE_GAP,
  // 120px thumbnail width
  THUMB_WIDTH,
  // 164px sidebar width
  SIDEBAR_WIDTH,
  // 768px
  MOBILE_BREAKPOINT,
  // 2 pages ahead/behind
  PRELOAD_DISTANCE,
  // 5 pages before unloading
  VIRTUALIZE_DISTANCE,
} from '@joelouf/doc-viewer/core';

ImageLoadController

Manages per-image state machines (idle → loading → loaded | error), preloading of adjacent pages, retry logic, and virtualization.

import { ImageLoadController, ImageState } from '@joelouf/doc-viewer/core';

const loader = new ImageLoadController({
  urls: ['/page-1.png', '/page-2.png', '/page-3.png'],
  onStateChange: (index, state, img) => {
    // State is one of: ImageState.IDLE, LOADING, LOADED, ERROR
  },
});

// Load page 0 and its neighbors
loader.loadWithNeighbors(0);

// Retry a failed page
loader.retry(2);

// Unload a distant page to free memory
loader.virtualize(5);

// Clean up
loader.destroy();

ReducedMotionController

Listens to the prefers-reduced-motion media query and detects runtime changes (e.g., user toggles the OS setting while the viewer is open).

import { ReducedMotionController } from '@joelouf/doc-viewer/core';

const ctrl = new ReducedMotionController((reduced) => {
  console.log('Reduced motion:', reduced);
});

// Current state
console.log(ctrl.matches);

// Remove listener
ctrl.destroy();

CSS Custom Properties

Style the viewer by setting CSS custom properties on the <document-page-viewer> element or any ancestor:

| Property | Default | Description | |---|---|---| | --dpv-bg | #525659 | Viewport background | | --dpv-toolbar-bg | #2b2d31 | Toolbar background | | --dpv-toolbar-text | #d4d4d8 | Toolbar text and icon color | | --dpv-page-shadow | 0 2px 16px rgba(0,0,0,0.35) | Shadow around page frames | | --dpv-thumbnail-border | #52525b | Thumbnail border color | | --dpv-accent | #3b82f6 | Accent color (active states, focus rings) |

document-page-viewer {
  --dpv-bg: #1a1a2e;
  --dpv-toolbar-bg: #16213e;
  --dpv-accent: #e94560;
}

URL and Source Security

The viewer validates all page sources before use.

URL strings — only the following are accepted:

  • https:// URLs
  • Relative paths (/, ./, ../)
  • http://localhost and http://127.* (for local development)

javascript:, data:, and blob: URL strings are rejected. External images are loaded with crossorigin="anonymous".

Blob objects — accepted directly as page sources. The viewer creates object URLs internally via URL.createObjectURL() and revokes them automatically when pages change or the component disconnects. This is the recommended approach for dynamically generated page images (e.g., canvas-rendered previews).

All page content is rendered via <img> elements only - no innerHTML, iframe, embed, or object tags are used.

Browser Support

Chrome 90+, Firefox 90+, Safari 15.4+, Edge 90+, iOS Safari 15.4+, Android Chrome 90+.

License

MIT