- PDF Viewer
- PDF Toolbar
- PDF More Actions Menu
- PDF Floating Toolbar
- PDF Document Tabs
- PDF View
- PDF Document Grid
- PDF Document Info
- PDF Page Navigation
- PDF Zoom Controls
- PDF Undo/Redo Buttons
- PDF Keyboard Shortcuts
- PDF Bookmark Sidebar
- PDF Thumbnail Sidebar
- PDF Search Panel
- PDF Attachment List
- PDF Annotation Layer
- PDF Annotation Toolbar
- PDF Annotation Inspector
- PDF Annotation Selection Menu
- PDF Annotation Sidebar
- PDF Comment
- PDF Comment Layer
- PDF Comment Draft
- PDF Comment Button
- PDF Comment Sidebar
- PDF Redaction Toolbar
- PDF Capture Button
- PDF Form Fill Toggle
- PDF Signature Button
- PDF Stamp Button
The annotation layer with commenting wired in — compose before posting, an avatar pin on the page, and the thread that opens over it.
"use client";
import { createPluginRegistration } from "@embedpdf/core";About#
<PdfCommentLayer /> is
<PdfAnnotationLayer /> with the three
pieces of commenting already connected to it. Mount it as your page layer and a
comment behaves the way people expect a comment to behave; mount the bare
annotation layer and it doesn't.
A comment is a PDF Text annotation, and a Text annotation's default appearance is a note icon sitting on the page — a yellow one, in the annotation plugin's rendering. That's a faithful reading of the format and the wrong thing to show a reader. Every tool people actually comment in — Figma, Google Docs, Acrobat's own review mode — marks a comment with who left it and opens the conversation in place. Three things get you there:
<PdfCommentDraftLayer />— so clicking with the comment tool opens a composer instead of committing an empty note.<PdfCommentPin />throughcustomAnnotationRenderer— the avatar marker in place of the icon.<PdfCommentPopover />throughselectionMenu— the thread, anchored to the pin.
All three have to be present. Miss any one and comments fall back silently: a pin with no thread, a thread with no composer, an icon with neither. That wiring is the whole component — it exists because it was being copied by hand into every surface that could make a comment, and a copy that drifts is a surface that renders comments differently from the one next to it.
When to use which layer#
| Surface | Layer |
|---|---|
Anything mounting <PdfCommentButton /> | <PdfCommentLayer /> |
An annotation toolbar carrying textComment — which the default set does | <PdfCommentLayer /> |
| A document that may already contain comments | <PdfCommentLayer /> |
| A surface with no comment tool at all — signing, stamping, form filling, a read-only viewer | <PdfAnnotationLayer /> |
The last row is the real distinction, and it's worth keeping: the comment layer pulls in an avatar, a popover, a dropdown menu, and a textarea. A highlight-only surface shouldn't carry a thread UI nothing can reach.
Installation#
pnpm dlx shadcn@latest add @pdfviewcn/comment-layer
Usage#
Register the annotation plugin with an author.
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. `annotationAuthor` attributes each pin and reply.
const plugins = [
createPluginRegistration(AnnotationPluginPackage, {
annotationAuthor: "You",
}),
];Render it through pageLayers, once per page.
<PdfViewer
documents={[{ url: "/sample.pdf" }]}
plugins={plugins}
className="h-[720px]"
>
<PdfToolbar>
<PdfCommentButton />
</PdfToolbar>
<PdfViewerContent
pageLayers={({ documentId, pageIndex }) => (
<PdfCommentLayer documentId={documentId} pageIndex={pageIndex} />
)}
/>
</PdfViewer>That's the whole setup. pageLayers is called once per page with its own
documentId and pageIndex — pass both straight through, the same as for the
annotation layer.
Wrap the viewer in
<PdfCommentUserProvider> to set the current
author and resolve avatars, matching its currentUser.name to the plugin's
annotationAuthor so one person's pins and replies read as the same author.
Examples#
Default#
Arm the comment tool, click a page, and type. Nothing reaches the document until you send; once it's sent, clicking the pin reopens the thread.
"use client";
import { createPluginRegistration } from "@embedpdf/core";Keeping a selection menu for other annotations#
selectionMenu still works and still belongs to you — comment pins take the
thread, and everything else is handed to your callback:
<PdfCommentLayer
documentId={documentId}
pageIndex={pageIndex}
selectionMenu={(props) => (
<PdfAnnotationSelectionMenu documentId={documentId} {...props}>
<PdfCommentThreadButton
documentId={documentId}
annotation={props.context.annotation.object}
/>
</PdfAnnotationSelectionMenu>
)}
/>That's what PDF Viewer App and
PDF Annotator App both do: comments open their own
thread, and a highlight or a square gets the ordinary menu — with a
<PdfCommentThreadButton /> in it, so a
conversation can start on artwork too.
customAnnotationRenderer behaves the same way. Comment pins are taken first,
and your renderer sees every other annotation, falling back to the plugin's own
drawing when you return children.
Replies are Text annotations too, but they belong to a thread rather than to
the page. isPdfCommentPin matches them out, so a reply never draws a second
marker on top of the comment it answers.
The sidebar beside it#
<PdfCommentSidebar /> is an index, not
a reader: a row selects its pin and scrolls to it, and the thread opens on the
page. The panel needs this layer beside the content for that to go anywhere.
"use client";
import { createPluginRegistration } from "@embedpdf/core";API Reference#
PdfCommentLayer#
Every prop of
<PdfAnnotationLayer />,
unchanged, with two given a comment-aware default:
| Prop | Type | Default | Description |
|---|---|---|---|
documentId | string | — | The document being annotated. |
pageIndex | number | — | The page this instance draws. |
customAnnotationRenderer | CustomAnnotationRenderer | — | Called for every annotation that isn't a comment pin. Return children for the plugin's own drawing. |
selectionMenu | AnnotationSelectionMenuRenderFn | — | Called when the selected annotation isn't a comment pin. Return null for no menu. |
It renders a
<PdfCommentDraftLayer /> and a
<PdfAnnotationLayer /> as siblings, so the data-slot attributes and the DOM
are those two components'. There's no wrapper element of its own.