Tool selection for the annotation plugin — highlight, draw, comment, and shapes.
"use client";
import { createPluginRegistration } from "@embedpdf/core";About#
<PdfAnnotationToolbar /> selects which annotation tool is active. Picking a tool arms the viewer; drawing, placing, and editing are handled by the annotation plugin's own layer once a tool is armed.
This is the "what am I about to make" half of annotating. The other half — what it looks like — is <PdfAnnotationInspector />.
The plugin registers nineteen tools, more than a row holds flat. By default they're gathered into four flyouts plus a stamp — five controls reaching eighteen tools — grouped by the annotation families the PDF spec defines. The tool a flyout is showing stays one click; its neighbours cost two. How the flyout opens depends on how the row is spaced — see Spacing and the flyout chevron.
Annotation is a heavy-tier plugin, so <PdfViewer /> doesn't register it for you. Add the plugin and the layer that draws annotations onto the page, both shown below.
The toolbar renders nothing at all when the annotation plugin isn't registered. If it appears empty, that's the missing registration rather than a missing document.
Installation#
pnpm dlx shadcn@latest add @pdfviewcn/annotation-toolbar
Usage#
Register the annotation plugin.
import { createPluginRegistration } from "@embedpdf/core";
import { AnnotationPluginPackage } from "@embedpdf/plugin-annotation/react";
// Outside the component — a new array identity on every render tears the
// engine down and rebuilds it, losing scroll and zoom state.
const plugins = [createPluginRegistration(AnnotationPluginPackage)];Render the page layer and the toolbar.
<PdfViewer
documents={[{ url: "/sample.pdf" }]}
plugins={plugins}
className="h-[720px]"
>
<PdfToolbar>
<PdfAnnotationToolbar />
</PdfToolbar>
<PdfViewerContent
pageLayers={({ documentId, pageIndex }) => (
<PdfCommentLayer documentId={documentId} pageIndex={pageIndex} />
)}
/>
</PdfViewer>Without the layer, tools still arm and annotations are still created — they just aren't drawn.
The layer here is <PdfCommentLayer /> rather than <PdfAnnotationLayer /> because textComment is in the default tool set: a comment is a Text annotation, and the bare layer draws it the way the spec describes — a yellow note icon — instead of as the avatar pin that opens a thread. Drop textComment from tools and the bare layer is the right one.
Examples#
Default#
"use client";
import { createPluginRegistration } from "@embedpdf/core";Choosing which tools to show#
Pass tools to choose your own set, in your own order. A string is one tool, rendered as a plain toggle:
<PdfAnnotationToolbar tools={["highlight", "underline", "ink"]} />An array is several tools sharing one flyout. The toggle shows one of them and opens a menu holding the rest:
<PdfAnnotationToolbar
tools={[["highlight", "underline", "strikeout"], "ink", "textComment"]}
/>Nesting is the whole grouping mechanism, so tools stays serialisable either way.
Ids the plugin doesn't know are skipped rather than rendered empty, so narrowing the list is safe when a tool has been removed from the plugin's own configuration. A group left with one surviving id renders as a plain toggle.
The full set of built-in ids: highlight, underline, strikeout, squiggly, insertText, replaceText, ink, inkHighlighter, circle, square, line, lineArrow, polyline, polygon, textComment, freeText, freeTextCallout, stamp, link. This set is embedpdf's; see its Annotation plugin for what each tool does and how to configure them.
The default groups#
TOOL_GROUPS is exported so you can reuse a group, drop one, or reorder them without retyping its members:
import {
PdfAnnotationToolbar,
TOOL_GROUPS,
} from "@/components/pdf-annotation-toolbar";
// Markup and shapes only, with stamp beside them.
<PdfAnnotationToolbar
tools={[TOOL_GROUPS.markup, TOOL_GROUPS.shape, "stamp"]}
/>;| Group | Tools | Spec family |
|---|---|---|
markup | highlight, underline, strikeout, squiggly, insertText, replaceText | Text markup (ISO 32000-1 §12.5.6.10) plus the caret edit intents (§12.5.6.11) |
draw | ink, inkHighlighter | Ink (§12.5.6.13) |
note | textComment, freeText, freeTextCallout | Text (§12.5.6.4) and Free Text (§12.5.6.6) |
shape | square, circle, lineArrow, line, polygon, polyline | Square/Circle (§12.5.6.8), Line (§12.5.6.7), Polygon/PolyLine (§12.5.6.9) |
form | formTextField, formCheckbox, formRadioButton, formCombobox, formListbox | Widget (§12.5.6.19), the on-page half of an interactive form field (§12.7) |
Grouping by spec family rather than by the plugin's tool list means the groups survive an embedpdf major even where the tool ids inside them don't.
stamp (§12.5.6.12) sits outside them as a plain toggle — a family of one. link is left out of the default: arming it would draw a link annotation with no URI editor to finish it.
form is left out because its five tools don't come from the annotation plugin at all — the form plugin adds them to the annotation registry when both are registered, so the group resolves to nothing when only annotation is. Opt into it where authoring fields is the point:
import {
DEFAULT_TOOLS,
PdfAnnotationToolbar,
TOOL_GROUPS,
} from "@/components/pdf-annotation-toolbar";
<PdfAnnotationToolbar tools={[...DEFAULT_TOOLS, TOOL_GROUPS.form]} />;Placing a field only draws its rectangle. Pair the group with <PdfFormFieldInspector />, which edits the field dictionary behind it — the name it's known by, the flags that constrain it, and the options a dropdown offers.
Spacing and the flyout chevron#
At the default spacing={0} the row is one welded segmented strip and a flyout's chevron folds into the tool's own toggle. One click arms the offered tool and opens the menu together — dense, which is what a docked row wants, but it costs the tool its second click. Clicking an armed tool again is how you put it down, and here that click is busy opening a menu.
Open the spacing and the chevron moves out into a button of its own, paired with the tool in a <ButtonGroup />:
<PdfAnnotationToolbar variant="default" size="lg" spacing={1} />"use client";
import { createPluginRegistration } from "@embedpdf/core";Two targets, no shared gesture: the tool arms and disarms like every plain toggle, and the chevron only opens the menu. It's the shape design tools use, and it suits the same situation — a surface you keep one tool armed on for minutes at a time.
The chevron follows spacing rather than a prop of its own because the two can't disagree. A welded strip's seam is built from first- and last-child radii on the group's direct children, and pairing a tool with a chevron nests both a level down, which puts every face in first position and breaks the strip.
A split chevron is a button beside the toggle group rather than an item in it, so it takes a tab stop of its own — see Accessibility.
Styling what you draw#
The toolbar arms a tool; it doesn't style one. Pair it with <PdfAnnotationInspector />, which edits the armed tool's defaults while nothing is selected and restyles the selection once something is:
<PdfToolbar>
<PdfAnnotationToolbar />
</PdfToolbar>
<div className="flex min-h-0 flex-1">
<PdfViewerContent className="flex-1" pageLayers={/* … */} />
<PdfAnnotationInspector className="w-72 border-l" />
</div>When annotating is disabled#
The toolbar reads the active document's modify-annotations permission and disables every tool — with a tooltip explaining why — when the flag is denied. 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 { createPluginRegistration } from "@embedpdf/core";Accessibility#
The toolbar is a single-select <ToggleGroup />, so arrow keys move between tools and the armed tool is exposed through its pressed state rather than colour alone. Each tool is icon-only and carries an aria-label with the tool's name, matching its tooltip, so the label is available whether or not the tooltip is shown.
A flyout is a radio menu — one checked item, the armed tool — so the choice is announced as a choice rather than as six unrelated commands, and whatever opens it carries aria-haspopup="menu".
At spacing={0} that trigger is the tool's own toggle — a real item in the group, so it keeps its place in the arrow-key order and its pressed state. It gives up disarming on a second click, since that click is opening the flyout. Plain toggles keep click-again-to-disarm either way.
Above spacing={0} the trade runs the other way: every tool disarms on a second click, and the chevron is a separate button named "<tool> options". A toolbar is meant to be one tab stop with arrow keys inside, and a chevron beside the group isn't an item in it, so each split group adds a tab stop. Nothing becomes unreachable — arrow keys pressed on a chevron still move into the tools — but the tab path is longer. Prefer the docked spacing where the row is dense and mostly driven by keyboard.
The chevron is 20px wide, under WCAG 2.5.8's 24px minimum on that axis. It spans the tool's full height, so it's larger in area than a compliant 24×24 square, but that's a trade rather than a pass — widen it to w-6 if your audit doesn't take it.
Annotating requires a pointer — tools arm from the keyboard, but drawing on the page has no keyboard equivalent.
API Reference#
PdfAnnotationToolbar#
A single-select <ToggleGroup /> carrying data-slot="pdf-annotation-toolbar", bound to the annotation plugin's active tool. Accepts every <ToggleGroup /> prop except value, which it owns.
| Prop | Type | Default | Description |
|---|---|---|---|
tools | PdfAnnotationToolSlot[] | The four TOOL_GROUPS + stamp | Slots to render, in order. A string is one tool; an array is a flyout over several. |
type PdfAnnotationToolSlot = string | string[];Returns null when the document isn't loaded or the annotation plugin isn't registered. Selecting the armed tool again disarms it — unless it's a flyout face at spacing={0}, whose second click is busy opening the menu.
| Slot | Element |
|---|---|
pdf-annotation-toolbar | The toggle group. |
pdf-annotation-toolbar-item | A plain toggle, and each flyout menu item. |
pdf-annotation-toolbar-group | A flyout's face. |
pdf-annotation-toolbar-trigger | A split flyout's chevron. Absent at spacing={0}. |
Icons are mapped from tool id; a tool added through the plugin's addTool renders with a generic shape icon rather than being dropped.
A flyout shows whichever member is armed, falling back to whichever was armed last, then to its first — so reaching for the same tool twice stays one click.
TOOL_GROUPS#
Flyout membership keyed markup, draw, note, shape, and form. Exported so a custom tools can reuse a group without restating it. See The default groups.
DEFAULT_TOOLS#
What tools falls back to: the first four groups plus stamp. Spread it to add to the default instead of restating it.