0

PDF View

Scope a subtree to one view, so multiple panes can read different documents side by side.

"use client";

import { useAllViews } from "@embedpdf/plugin-view-manager/react";

About

<PdfView /> scopes a subtree to a single view — one pane of a multi-pane viewer. It shadows the ambient view context that <PdfViewer> provides, so a <PdfToolbar>, <PdfDocumentTabs>, or <PdfViewerContent> inside resolves that pane's active document rather than the focused pane's. It's built on the same view-manager that powers document tabs:

  • Scope — a pane's chrome reads its own document. Paging or switching a tab in one pane never disturbs the other.
  • Focus — pointer-down anywhere in a pane focuses its view and stamps data-focused. Chrome rendered outside every <PdfView> — a shared sidebar, an app-level toolbar — reads the focused pane, so it follows the pane you're working in.

Two controls move documents between panes:

  • <PdfMoveToNewPaneButton /> creates a pane beside the current one and moves the active document into it. Disabled until the pane holds more than one document, since moving one out is only meaningful while another stays behind.
  • <PdfClosePaneButton /> collapses a pane, moving its documents into a sibling rather than orphaning them. Disabled while only one pane exists.

Layout is yours

pdfcn ships no layout component for panes, on purpose. useAllViews() gives you the views and <PdfView> scopes one; where they go is your call — a <ResizablePanelGroup>, a CSS grid, a tab strip, a portal into a second browser window. Baking a resizable group into the component would make one arrangement the only arrangement, and would put another library's props in pdfcn's public type.

Installation

pnpm dlx shadcn@latest add pdfcn/view

Usage

Map useAllViews() into your layout of choice, and wrap each pane's chrome in a <PdfView>.

function Panes() {
  const views = useAllViews();
 
  return (
    <ResizablePanelGroup>
      {views.map((view, index) => (
        <React.Fragment key={view.id}>
          {index > 0 ? <ResizableHandle withHandle /> : null}
          <ResizablePanel id={view.id} className="min-w-0">
            <PdfView viewId={view.id}>
              <PdfDocumentTabs />
              <PdfToolbar>
                <PdfPageNavigation />
                <PdfZoomControls />
                <div className="ml-auto flex items-center gap-1">
                  <PdfMoveToNewPaneButton />
                  <PdfClosePaneButton />
                </div>
              </PdfToolbar>
              <PdfViewerContent />
            </PdfView>
          </ResizablePanel>
        </React.Fragment>
      ))}
    </ResizablePanelGroup>
  );
}

Render it inside <PdfViewer>. The map needs a child component, since useAllViews() reads the plugin registry and <PdfViewer> mounts its children only once the engine is ready.

<PdfViewer
  documents={[{ url: "/sample.pdf" }, { url: "/form.pdf" }]}
  className="h-[720px]"
>
  <Panes />
</PdfViewer>

Examples

Default

Two documents load in one pane. Moving one out opens a second pane; each pane's toolbar and tabs act only on its own document. Drag the handle to resize, and close a pane to merge its documents back.

"use client";

import { useAllViews } from "@embedpdf/plugin-view-manager/react";

Shared sidebar

Chrome outside every <PdfView> reads the focused pane, which is how one sidebar can serve every pane — click into a pane and the sidebar follows. Put the same sidebar inside a <PdfView> instead and it pins to that pane.

That pairing is the whole implementation of a pin: hold the pinned view's id in state, and wrap the sidebar's body in a <PdfView> only while it's set.

const [pinnedViewId, setPinnedViewId] = React.useState<string | null>(null);
 
return pinnedViewId ? <PdfView viewId={pinnedViewId}>{body}</PdfView> : body;

Read the pin as live only while its view is (views.some((view) => view.id === pinnedViewId)) and closing the pinned pane reverts the sidebar to following focus on its own, with no effect to keep the two in step. <PdfWorkspaceApp /> ships both halves — a shared sidebar with a pin toggle in its header.

Accessibility

The two controls are single buttons labelled "Move to new pane" and "Close pane", each with a matching tooltip, and each disabled rather than hidden while it can't act — so the toolbar never reflows under the pointer.

Focus follows pointer-down, not keyboard focus. When panes are arranged in a resizable group, the drag handle is keyboard operable: focus it and use the arrow keys to resize.

API Reference

PdfView

A <div /> carrying data-slot="pdf-view" and data-focused. Accepts every prop a div takes, plus:

PropTypeDescription
viewIdstringThe view-manager view this subtree is scoped to.

PdfMoveToNewPaneButton

A <Button /> carrying data-slot="pdf-move-to-new-pane-button". Creates a new pane and moves the current pane's active document into it. Disabled until the pane holds more than one document. Accepts every prop the button takes.

PdfClosePaneButton

A <Button /> carrying data-slot="pdf-close-pane-button". Merges the current pane's documents into a sibling and removes the pane. Disabled while only one pane exists. Accepts every prop the button takes.