0

PDF Floating Toolbar

A tool pill that floats over the page canvas instead of docking above it, pinned to any edge.

"use client";

import { createPluginRegistration } from "@embedpdf/core";

About

<PdfFloatingToolbar /> is the design-tool shape of a toolbar: the document is the interface, and the controls hover on top of it. It's a sibling of <PdfToolbar />, not a variant of it.

The difference is a layout contract, not a look. <PdfToolbar /> sits in the layout's flow, spans the full width, and pushes the canvas down. This one is taken out of flow and pinned to an edge, so it owns a placement — side and align — that a docked bar has no use for, and costs the canvas no height.

Both are deliberately dumb. Neither owns document state or knows what's inside it, so the same controls work in either.

Installation

pnpm dlx shadcn@latest add pdfcn/floating-toolbar

Usage

import {
  PdfFloatingToolbar,
  PdfFloatingToolbarSeparator,
} from "@/components/pdf-floating-toolbar";
<div className="relative flex min-h-0 flex-1">
  <PdfViewerContent className="flex-1" />
  <PdfFloatingToolbar>
    <PdfPointerToggle size="lg" />
    <PdfPanToggle size="lg" />
    <PdfFloatingToolbarSeparator />
    <PdfAnnotationToolbar variant="default" size="lg" spacing={1} />
  </PdfFloatingToolbar>
</div>

Controls take size="lg" here rather than the size="sm" they default to. A docked bar is dense because it shares a row with everything else; a floating pill is a target you aim at, so it's built at the larger size.

<PdfAnnotationToolbar /> also drops its segmented borders (variant="default") and opens its spacing (spacing={1}), so its tools read as separate pills rather than one welded block.

Examples

Default

"use client";

import { createPluginRegistration } from "@embedpdf/core";

Choosing an edge

side picks the edge to pin to and align picks where along it to sit. A pill on the top or bottom edge runs horizontally; one on the left or right runs vertically, and its separators turn with it.

<PdfFloatingToolbar side="left" align="center">
  {/* … */}
</PdfFloatingToolbar>

Tooltips follow: they open on the far side of the pill from the edge it's anchored to, so they open into the page rather than into the edge of the viewer, where they'd have to flip or be clipped. A pill along the bottom labels upward; one along the left labels rightward.

To move it further from the edge, pass a class — side and align exist because they change which edges and transforms apply, and an offset is one class:

<PdfFloatingToolbar className="bottom-8" />

One mutually exclusive set

The interaction manager holds exactly one active mode per document, and every mode control arms one — pointer and pan from the interaction manager, drawing tools from the annotation plugin. So a row of them behaves as a single mutually exclusive set even though each reads its own plugin, and nothing here has to coordinate it:

<PdfFloatingToolbar>
  <PdfPointerToggle size="lg" />
  <PdfPanToggle size="lg" />
  <PdfFloatingToolbarSeparator />
  <PdfAnnotationToolbar
    variant="default"
    size="lg"
    spacing={1}
    tools={["square", "circle", "freeText", "highlight", "ink", "textComment"]}
  />
</PdfFloatingToolbar>

<PdfPointerToggle /> is the way back to none of them — it reads as pressed whenever nothing else is armed.

Adding your own tool

The registry's controls already label themselves and drop straight in. <PdfFloatingToolbarItem /> is for the ones you add yourself: it matches their size and tooltip behaviour, so a custom tool doesn't read as a foreign object in the row.

<PdfFloatingToolbar>
  <PdfPointerToggle size="lg" />
  <PdfFloatingToolbarSeparator />
  <PdfFloatingToolbarItem
    label="Measure"
    pressed={tool === "measure"}
    onPressedChange={(pressed) => setTool(pressed ? "measure" : null)}
  >
    <RulerIcon />
  </PdfFloatingToolbarItem>
</PdfFloatingToolbar>

It's a toggle because everything in a tool pill is a mode — you arm it and it stays armed. For a one-shot action, put a plain <Button variant="ghost" size="icon-lg" /> in the toolbar instead; it's a dumb container and won't mind.

Alongside a docked toolbar

The two aren't alternatives. A common split is document chrome docked at the top — paging, zoom, export — with the tools floating over the page:

<PdfViewer documents={[{ url: "/sample.pdf" }]} className="h-[720px]">
  <PdfToolbar>
    <PdfPageNavigation />
    <PdfToolbarSeparator />
    <PdfZoomControls />
  </PdfToolbar>
  <div className="relative flex min-h-0 flex-1">
    <PdfViewerContent className="flex-1" />
    <PdfFloatingToolbar>{/* … */}</PdfFloatingToolbar>
  </div>
</PdfViewer>

<PdfAnnotatorApp /> is this layout in full.

Accessibility

<PdfFloatingToolbar /> carries role="toolbar" and an aria-orientation matching the edge it's pinned to, so assistive technology reads a vertical rail as vertical.

Every item is icon-only, so each carries an aria-label — the same text its tooltip shows, which means the label is available whether or not the tooltip opens. <PdfFloatingToolbarItem /> takes one label prop and uses it for both, so the two can't drift apart.

Because the pill is absolutely positioned, it appears in tab order where it sits in the DOM, not where it sits on screen. Place it after <PdfViewerContent /> — as the examples do — so tabbing reaches the document before the tools that act on it.

API Reference

PdfFloatingToolbar

An absolutely positioned flex container carrying data-slot="pdf-floating-toolbar" and role="toolbar", plus data-side and data-orientation. Accepts every div prop.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"The edge of the positioned ancestor to pin to.
align"start" | "center" | "end""center"Where along that edge to sit.

Orientation is derived from side rather than set: left and right are vertical, top and bottom horizontal. There's no combination where a pill runs along the edge it's pinned to.

PdfFloatingToolbarItem

A <Toggle /> carrying data-slot="pdf-floating-toolbar-item", wrapped in a tooltip. Accepts every <Toggle /> prop, plus:

PropTypeDefaultDescription
labelstringThe accessible name, and the text its tooltip shows.
tooltipSide"top" | "right" | "bottom" | "left"Away from sideOverrides which side the tooltip opens on.

Defaults to size="lg", the size a floating pill is built at.

PdfFloatingToolbarSeparator

A <Separator /> carrying data-slot="pdf-floating-toolbar-separator", sized to the height of a control rather than the full pill. Accepts every <Separator /> prop.

It crosses the pill, so it takes the opposite orientation to the toolbar it's in — read from context, so a rail's separators turn with it and nothing has to be passed down by hand.