0

PDF Toolbar

A layout shell and a set of controls for paging, zooming, rotating, panning, printing, and downloading a document.

"use client";

import { PdfPageNavigation } from "@/components/pdf-page-navigation";

About

<PdfToolbar /> is a layout shell that owns no state. Each control is independent, reading the active document from <PdfViewer />'s context — so compose the ones you want, drop the ones you don't, and mix in your own buttons.

The controls drive plugins <PdfViewer /> already registers, so adding a toolbar doesn't change which plugins load.

Installation

pnpm dlx shadcn@latest add pdfcn/toolbar

This installs the shell together with every control. The controls also ship on their own, without the shell:

pnpm dlx shadcn@latest add pdfcn/toolbar-controls

That's one file holding pointer, pan, rotate, spread, print, download, fullscreen, and open — each is a single plugin hook wired to a single shadcn control, so splitting them apart bought seven copies of the same twenty lines. Delete the ones you don't render. Page navigation, zoom, and undo/redo carry enough behaviour to stay separate, and have their own pages.

Usage

import { PdfPageNavigation } from "@/components/pdf-page-navigation";
import { PdfToolbar, PdfToolbarSeparator } from "@/components/pdf-toolbar";
import {
  PdfExportButton,
  PdfFullscreenToggle,
  PdfPrintButton,
} from "@/components/pdf-toolbar-controls";
import { PdfZoomControls } from "@/components/pdf-zoom-controls";
<PdfViewer documents={[{ url: "/sample.pdf" }]} className="h-[720px]">
  <PdfToolbar>
    <PdfPageNavigation />
    <PdfToolbarSeparator />
    <PdfZoomControls />
  </PdfToolbar>
  <PdfViewerContent />
</PdfViewer>

Order matters only for layout. <PdfToolbar /> is shrink-0 and <PdfViewerContent /> is flex-1, so the toolbar keeps its height and the document takes the rest — put the toolbar above the content, below it, or both.

The toolbar is a plain flex row, so grouping and alignment are yours to set. A wrapper with ml-auto pushes document-level actions to the far end:

<PdfToolbar>
  <PdfPageNavigation />
  <PdfToolbarSeparator />
  <PdfZoomControls />
  <div className="ml-auto flex items-center gap-1">
    <PdfPrintButton />
    <PdfExportButton />
    <PdfFullscreenToggle />
  </div>
</PdfToolbar>

Examples

Default

"use client";

import { PdfPageNavigation } from "@/components/pdf-page-navigation";

When printing is disabled

<PdfPrintButton /> reads the active document's print permission and disables itself — with a tooltip explaining why — when the flag is denied. Download isn't gated by a permission flag, so it stays enabled alongside. The demo below forces the flag off with a permissions override; a real document carries it in its own flags. See Permissions for the full model.

"use client";

import { PdfToolbar } from "@/components/pdf-toolbar";

Accessibility

<PdfToolbar /> carries role="toolbar". Every control is a real <button> or <input>, reachable with Tab and operable with Enter or Space. Icon-only buttons carry an aria-label matching their tooltip, so the label is available whether or not the tooltip is shown.

Controls render disabled rather than absent while the document loads, so tab order doesn't shift once it resolves.

API Reference

PdfToolbar

A styled flex row carrying data-slot="pdf-toolbar" and role="toolbar". Accepts every div prop. It renders whatever you put in it and nothing else — there is no overflow or responsive collapsing behavior.

PdfToolbarGroup

A rounded, muted well that wraps a run of related controls so they read as one cluster. Accepts every div prop.

PdfToolbarSeparator

A vertical <Separator /> sized for the toolbar's height. Accepts every <Separator /> prop.

PdfPageNavigation

Previous and next page buttons around an editable page field. See PDF Page Navigation for props, installation, and examples.

PdfZoomControls

Zoom out and zoom in buttons around a dropdown of zoom levels. See PDF Zoom Controls for props, installation, and examples.

PdfRotateButton

Rotates the document 90° clockwise via useRotate. Accepts every <Button /> prop, including children to swap the icon.

PdfSpreadToggle

A <ToggleGroup /> switching between single page, two pages, and two pages with cover via useSpread. Accepts every <ToggleGroup /> prop.

PdfPointerToggle

A <Toggle /> returning the viewer to plain pointing — selecting text, picking annotations, following links. Accepts every <Toggle /> prop, including children to swap the icon.

The counterpart to every mode control. The interaction manager holds exactly one active mode per document, and <PdfPanToggle /> and each annotation tool arm one, so this is the way back to none of them. It reads as pressed whenever nothing else is armed, which makes a row of pointer, pan, and annotation tools behave as one mutually exclusive set even though each reads its own plugin.

Pressing it when it's already pressed leaves it pressed rather than arming nothing — "no tool" is a state, not the absence of one.

PdfPanToggle

A <Toggle /> arming drag-to-pan via usePan. Accepts every <Toggle /> prop, including children to swap the icon.

Panning is an interaction mode, not a setting: arming it disarms whatever mode was active — text selection, an annotation tool. The toggle follows the plugin's state, so it turns itself off when something else takes over.

Panning can also be the default mode rather than an armed one — the way a touch device scrolls, where dragging moves the page and there's nothing to arm. Set it where the plugin is registered:

createPluginRegistration(PanPluginPackage, { defaultMode: "mobile" });

"mobile" applies it on touch devices only, "always" everywhere, "never" (the default) leaves panning armed on demand. <PdfPointerToggle /> tracks whatever the default currently is, so it stays correct either way.

PdfPrintButton

Prints the document via usePrint. Accepts every <Button /> prop.

The whole document is rasterized before the print dialog opens, which is slow on a long one, so the button shows a spinner and disables itself until the job is ready. The hidden iframe it renders into is mounted by the print plugin.

PdfExportButton

Downloads the document via useExport. Accepts every <Button /> prop.

What's saved is the document as it currently stands — annotations and rotations included — not the bytes originally fetched. The hidden anchor that triggers the download is mounted by the export plugin.

The saved file takes its name from the document, falling back to document.pdf for one loaded by URL. Change the fallback where the plugin is registered in <PdfViewer />:

createPluginRegistration(ExportPluginPackage, {
  defaultFileName: "report.pdf",
});

See embedpdf's Export plugin for the rest of its options.

PdfFullscreenToggle

Expands <PdfViewer /> to fill the screen via useFullscreen, and collapses it again. Accepts every <Button /> prop.

The odd one out: fullscreen belongs to the viewer, not a document, so it has no document to wait for and is enabled as soon as the plugin is ready. It requests the change and <PdfViewer /> expands its own root in response — which is also why the button stays correct when a user leaves fullscreen with Esc.

PdfOpenFileButton

Opens a file picker and loads the chosen PDF via the document manager plugin. Accepts every <Button /> prop, plus:

PropTypeDefaultDescription
closeExistingbooleanfalseClose every open document before showing the picker, so exactly one ends up loaded.

The other control that isn't document-scoped: opening a file has to work before anything is loaded, which is the one moment it matters most, so it's enabled as soon as the plugin is ready.

closeExisting is off by default because the underlying capability is inherently multi-document. A single-document host opts in; a tabbed one leaves it off so the new file opens alongside what's already there. Either way the picker opens even if closing fails, so a stuck document can't lock the user out of loading a replacement.

PdfUndoRedoButtons

Undo and redo buttons driven by the history plugin. See PDF Undo/Redo Buttons for props, installation, and examples.

Rendering controls outside a toolbar

Every control is independent of <PdfToolbar /> and only needs a <PdfViewer /> above it, so they can be placed in a sidebar, a floating overlay, or a menu:

<PdfViewer documents={[{ url: "/sample.pdf" }]} className="relative h-[720px]">
  <PdfViewerContent />
  <PdfPageNavigation className="bg-background absolute bottom-4 left-1/2 -translate-x-1/2 rounded-lg border p-1 shadow-md" />
</PdfViewer>

For a whole cluster of them over the page, <PdfFloatingToolbar /> is the shell built for it — same controls, pinned to an edge instead of docked in the flow.

Waiting for the document

embedpdf's document-scoped hooks — each keyed by a documentId — throw when handed a document with no plugin state yet, so each control renders a disabled shell until it's loaded. Follow the same split in your own controls: gate on the document first, then call the plugin hook in a child that takes a non-null documentId.