0

PDF Comment Layer

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:

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

SurfaceLayer
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.

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:

PropTypeDefaultDescription
documentIdstringThe document being annotated.
pageIndexnumberThe page this instance draws.
customAnnotationRendererCustomAnnotationRendererCalled for every annotation that isn't a comment pin. Return children for the plugin's own drawing.
selectionMenuAnnotationSelectionMenuRenderFnCalled 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.