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. A sibling of <PdfToolbar />, not a variant.
The difference is a layout contract, not a look. <PdfToolbar /> sits in the flow, spans the full width, and pushes the canvas down. This one is out of flow and pinned to an edge, so it takes a side and an align and costs the canvas no height. Neither owns document state, so the same controls work in either.
It positions itself absolutely, so it needs a relative ancestor — normally
the element wrapping the viewer content. Without one it pins to whatever
positioned element it finds further up, which is usually the page.
Installation#
pnpm dlx shadcn@latest add @pdfviewcn/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 their default size="sm". A docked bar is dense because it shares a row; a floating pill is a target you aim at.
<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. Opening the spacing also splits each group's chevron out into a button of its own — a pill is a surface you keep a tool armed on, so the click that puts it down has to be the same click that picked it up.
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 one active mode per document, and every mode control arms it — pointer and pan, plus every drawing tool. So a row of them behaves as one mutually exclusive set with nothing here coordinating 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, drop a plain <Button variant="ghost" size="icon-lg" /> in instead.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
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:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The accessible name, and the text its tooltip shows. |
tooltipSide | "top" | "right" | "bottom" | "left" | Away from side | Overrides 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.