0

Building blocks for PDF viewers

Whole readers assembled from pdfviewcn's components — a toolbar, sidebars, search, and annotation on embedpdf's engine. Preview one, then copy the source and make it yours.

Files
app/viewer/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfViewerApp = dynamic(
  () =>
    import("@/components/pdf-viewer-app").then(
      (mod) => mod.PdfViewerApp,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The viewer fills its container and scrolls inside itself, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      <PdfViewerApp documents={[{ url: "/sample.pdf" }]} />
    </div>
  );
}
Files
app/annotator/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfAnnotatorApp = dynamic(
  () =>
    import("@/components/pdf-annotator-app").then(
      (mod) => mod.PdfAnnotatorApp,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The editor fills its container and scrolls inside itself, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      <PdfAnnotatorApp documents={[{ url: "/sample.pdf" }]} />
    </div>
  );
}
Files
app/workspace/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfWorkspaceApp = dynamic(
  () =>
    import("@/components/pdf-workspace-app").then(
      (mod) => mod.PdfWorkspaceApp,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The workspace fills its container and scrolls inside itself, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      {/* Two documents so the tab strip and the split panes have something to
          work with from the first frame. */}
      <PdfWorkspaceApp
        documents={[
          { url: "/sample.pdf", name: "sample.pdf" },
          { url: "/form.pdf", name: "form.pdf" },
        ]}
      />
    </div>
  );
}
Files
app/compare/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfCompareApp = dynamic(
  () =>
    import("@/components/pdf-compare-app").then(
      (mod) => mod.PdfCompareApp,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The panes fill their container and scroll inside themselves, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      {/* Two revisions of the same eight pages, a handful of sentences apart —
          both generated by `scripts/build-sample-pdf.ts` from one set of page
          specs, so the panes line up and the edits are the only difference to
          spot. */}
      <PdfCompareApp
        documents={[
          { url: "/sample.pdf", name: "sample.pdf" },
          { url: "/sample-revised.pdf", name: "sample-revised.pdf" },
        ]}
      />
    </div>
  );
}
Files
app/library/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfLibraryApp = dynamic(
  () =>
    import("@/components/pdf-library-app").then(
      (mod) => mod.PdfLibraryApp,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The library fills its container and scrolls inside itself, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      {/* Three documents, so the grid opens on a shelf rather than a single
          card. */}
      <PdfLibraryApp
        documents={[
          { url: "/sample.pdf", name: "sample.pdf" },
          { url: "/form.pdf", name: "form.pdf" },
          { url: "/sample-with-attachments.pdf", name: "attachments.pdf" },
        ]}
      />
    </div>
  );
}
Files
app/library-browser/page.tsx
"use client";

import dynamic from "next/dynamic";

// embedpdf's plugin packages and the PDFium worker are browser-only, so the
// whole app is loaded client-side behind an `ssr: false` boundary.
const PdfLibraryBrowser = dynamic(
  () =>
    import("@/components/pdf-library-browser").then(
      (mod) => mod.PdfLibraryBrowser,
    ),
  {
    ssr: false,
    loading: () => <div className="bg-muted h-full w-full" />,
  },
);

export default function Page() {
  return (
    // The browser fills its container and scrolls inside itself, so pin an
    // explicit height here rather than letting the document grow the page.
    <div className="h-svh w-full overflow-hidden">
      {/* Three documents, so the grid opens on a shelf rather than a single
          card. */}
      <PdfLibraryBrowser
        documents={[
          { url: "/sample.pdf", name: "sample.pdf" },
          { url: "/form.pdf", name: "form.pdf" },
          { url: "/sample-with-attachments.pdf", name: "attachments.pdf" },
        ]}
      />
    </div>
  );
}