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 the plugin draws one as a yellow note icon — faithful to the format, and not what a reader expects. Figma, Google Docs, and Acrobat's review mode all mark a comment with who left it and open 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 degrade silently: a pin with no thread, a thread with no composer, an icon with neither. That wiring is the whole component.
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: the comment layer pulls in an avatar, a popover, a dropdown menu, and a textarea, and 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";Accessibility#
This layer adds no chrome of its own, so accessibility is the three components it wires: the composer in PDF Comment Draft, and the pin and thread in PDF Comment.
What the layer decides is which of them is reachable: the bare annotation layer on a commenting surface leaves a note icon with no keyboard route into its text.
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.