A tablet showing a paginated document beside fanned paper sheets, with a calm gray-blue cat resting on the windowsill behind it.

PDFSlick: A Slick Way to View PDFs Across Every Framework

The Gray Cat
The Gray Cat
0 views

If you have ever tried to drop a PDF viewer into a React or Svelte app, you probably reached for PDF.js, the same rendering engine that powers Firefox's built-in viewer. It is rock solid, but its API is low-level and imperative, designed for a world before reactive components. Wiring it into a framework where state should flow downward and UI should update automatically is a genuine chore.

PDFSlick closes that gap. It wraps PDF.js into something that feels at home in modern component frameworks: ready-made viewer components plus a reactive store. You get a full-featured viewer out of the box, and every piece of document state, the current page, total page count, zoom scale, thumbnails, rotation, and more, lives in a subscribable store. That means you can build custom toolbars, page navigators, thumbnail sidebars, and search interfaces that stay perfectly in sync, while PDFSlick handles the rendering. It works across React, SolidJS, Svelte, and plain JavaScript, all sharing one core.

What Makes It Tick

PDFSlick is more "batteries included" than most PDF tooling, while staying refreshingly open. Here is what you get:

  • A drop-in viewer component that renders PDF.js output without manual canvas plumbing.
  • Thumbnails in vertical or horizontal layouts for quick page navigation.
  • Zoom controls via increaseScale(), decreaseScale(), and named scale values like page-fit, page-width, and auto, with responsive re-scaling through ResizeObserver.
  • Page navigation with gotoPage(n) and reactive pageNumber / numPages state.
  • In-document text search.
  • Annotations and markup, including comments and a color menu, prominent in the full viewer since v4.0.0.
  • Rotation, download/save, and print, all exposed through the store and instance.
  • Multiple documents rendered side by side on a single page.
  • Single-page or continuous scroll viewing, toggled with one option.

Under the hood the dependency tree stays lean: just pdfjs-dist and zustand. The store is what makes everything click; because state is reactive, custom UI becomes almost trivial to assemble.

Getting It Into Your Project

PDFSlick is a monorepo of framework-specific adapters around the shared @pdfslick/core. Install the one that matches your stack.

# React
npm install @pdfslick/react

# SolidJS
npm install @pdfslick/solid

# Vanilla JS or Svelte (core only)
npm install @pdfslick/core

With yarn:

yarn add @pdfslick/react
# or @pdfslick/solid, or @pdfslick/core

Each framework package ships its own stylesheet that you must import, for example @pdfslick/react/dist/pdf_viewer.css. Forget this and the viewer renders without its layout, so make it the first thing you add.

Your First Viewer in React

The React adapter centers on a single hook, usePDFSlick(). You hand it a file path and some options, and it returns everything you need: a ref to attach the viewer container, a selector hook for the reactive store, and the viewer component itself.

import "@pdfslick/react/dist/pdf_viewer.css";
import { usePDFSlick } from "@pdfslick/react";

type Props = {
  pdfFilePath: string;
};

function MyPDFViewer({ pdfFilePath }: Props) {
  const { viewerRef, usePDFSlickStore, PDFSlickViewer } = usePDFSlick(
    pdfFilePath,
    {
      singlePageViewer: true,
      scaleValue: "page-fit",
    }
  );

  const pageNumber = usePDFSlickStore((s) => s.pageNumber);
  const numPages = usePDFSlickStore((s) => s.numPages);
  const pdfSlick = usePDFSlickStore((s) => s.pdfSlick);

  return (
    <div>
      <button
        onClick={() => pdfSlick?.gotoPage(pageNumber - 1)}
        disabled={pageNumber <= 1}
      >
        Prev
      </button>
      <span>
        {pageNumber} / {numPages}
      </span>
      <button
        onClick={() => pdfSlick?.gotoPage(pageNumber + 1)}
        disabled={pageNumber >= numPages}
      >
        Next
      </button>
      <button onClick={() => pdfSlick?.increaseScale()}>Zoom +</button>
      <button onClick={() => pdfSlick?.decreaseScale()}>Zoom −</button>

      <PDFSlickViewer {...{ viewerRef, usePDFSlickStore }} />
    </div>
  );
}

The pattern here is the key thing to notice. usePDFSlickStore is a selector hook in the Zustand style: each call subscribes to exactly one slice of state, so your component re-renders only when that slice changes. The pdfSlick instance carries the imperative methods like gotoPage and increaseScale, while the reactive values like pageNumber drive your UI. You build the chrome; PDFSlick keeps it honest.

Driving the Core Directly

When you are not in React, or you simply want full control, @pdfslick/core exposes the machinery directly. You create a store, instantiate PDFSlick with a container element, and load a document.

import { create, PDFSlick } from "@pdfslick/core";

export function loadPdf(url = "/document.pdf") {
  const store = create();
  const container = document.querySelector<HTMLDivElement>("#viewerContainer");

  if (!container) return;

  const pdfSlick = new PDFSlick({
    container,
    store,
    options: { scaleValue: "page-fit" },
  });

  pdfSlick.loadDocument(url);
  store.setState({ pdfSlick });

  // Keep the scale responsive as the container resizes
  const resizeObserver = new ResizeObserver(() => {
    const { scaleValue } = store.getState();
    if (scaleValue && ["page-width", "page-fit", "auto"].includes(scaleValue)) {
      pdfSlick.viewer.currentScaleValue = scaleValue;
    }
  });
  resizeObserver.observe(container);
}

This is exactly how the Svelte integration works too: you lean on the core, pass it a container and a store, and subscribe to updates. The ResizeObserver snippet is worth keeping around. Named scale values like page-fit need to be reapplied when the container changes size, otherwise the document stops filling the available space gracefully.

Custom Controls Without a Framework

Because the store is the single source of truth, you can wire plain DOM controls to it using the same subscribe pattern Zustand users will recognize. Subscribe once to react to state, and call instance methods on click.

const unsubscribe = store.subscribe((s) => {
  previousBtn?.toggleAttribute("disabled", s.pageNumber <= 1);
  nextBtn?.toggleAttribute("disabled", s.pageNumber >= s.numPages);
});

zoomInBtn?.addEventListener("click", () =>
  store.getState().pdfSlick?.increaseScale()
);
zoomOutBtn?.addEventListener("click", () =>
  store.getState().pdfSlick?.decreaseScale()
);
nextBtn?.addEventListener("click", () =>
  store.getState().pdfSlick?.gotoPage(store.getState().pageNumber + 1)
);

The state surface you can subscribe to includes pageNumber, numPages, scaleValue (a named zoom or a numeric scale), the pdfSlick instance itself, plus thumbnails, loading state, and rotation. The instance methods cover loadDocument(url), gotoPage(pageNumber), increaseScale() / decreaseScale(), setting viewer.currentScaleValue directly, and the rotate, download, print, and search APIs. Whatever toolbar you can imagine, the store has the data to back it.

Reaching for SolidJS

The Solid adapter mirrors React's ergonomics but speaks Solid's language. Calling usePDFSlick() returns viewerRef, a signal-based pdfSlickStore, and the PDFSlickViewer component. State flows through Solid signals, so reads stay fine-grained and reactive without a selector hook. Remember to import @pdfslick/solid/dist/pdf_viewer.css, just as you would for the other adapters.

This consistency across frameworks is one of PDFSlick's quiet strengths. The mental model, a container ref plus a reactive store plus a viewer component, is identical whether you are in React, Solid, Svelte, or vanilla JS. Learn it once and it transfers everywhere.

What Arrived in v4.0.0

Version 4.0.0 is the headline release of the current era, and it brings a couple of meaningful changes. The biggest is the migration to pdfjs-dist v6. This is a breaking change, so projects pinned to an older PDF.js will need to bump and verify their setup. The payoff is staying current with the engine's rendering improvements and security fixes.

On the feature side, the full PDF viewer gained real annotation capabilities: markup, comments, and a color menu that auto-closes once you pick a shade. The release also tidied up the annotation example with viewport responsiveness fixes and better dark-mode text visibility, and synchronized dependencies for clean installs. If you have been waiting for built-in markup before adopting PDFSlick, this is the version that delivers it.

There is also a separate offering worth knowing about: PDFSlick Sync adds real-time collaboration, letting multiple users view and annotate the same document together. It sits alongside the core library rather than inside it, so you opt in only if you need shared, live document sessions.

How It Compares

PDFSlick lives in a crowded neighborhood, and it helps to know where it sits. The most popular React option, react-pdf, is also built on PDF.js but stays deliberately low-level: it renders Document and Page and leaves the toolbar, zoom, search, and thumbnails entirely to you. PDFSlick is the opposite philosophy, shipping a full viewer with those pieces included, plus a reactive store and multi-framework support.

@react-pdf-viewer/core is closer in scope, a feature-rich React viewer with a plugin architecture, but it is React-only and some advanced features have historically sat behind a commercial license. PDFSlick is fully MIT-licensed and framework-agnostic. And if you only need to generate PDFs from components rather than view them, that is a different problem entirely, solved by tools like @react-pdf/renderer, not by PDFSlick.

The honest trade-offs: PDFSlick has a smaller community and download base than react-pdf, and it inherits PDF.js's bundle weight. But if you want a polished, open viewer with thumbnails, search, zoom, and annotations working out of the box, and you would like the same approach to carry across React, Solid, Svelte, and vanilla JS, PDFSlick is a remarkably comfortable fit. It takes the hardest, most imperative part of PDF rendering and hands you a reactive store instead. That is a trade most teams will happily make.