PDF Comment Sidebar

A newest-first index of every conversation on the document — author, first line, reply count, and review status. Click a row to select its pin and scroll to it.

"use client";

import { createPluginRegistration } from "@embedpdf/core";

About

<PdfCommentSidebar /> lists every thread in a document so a reader can scan them and jump to any one. It's the navigational counterpart to the on-page <PdfCommentPopover />: the panel finds a thread, the popover reads and replies to it. Rows come straight from plugin state through getPdfCommentThreads, so the list always mirrors the page.

  • A preview per thread — participants as an avatar stack, a #N · Page N reference, the author, relative time, the first few lines, and a reply count.
  • Where a thread landed — a row wears its review status once someone has ruled on it. PDF keeps status per author, so the row shows the latest from anybody; open the thread to change your own.
  • Conversations, not annotations — a square you drew isn't a comment and never appears here, but a square someone replied to does. See What counts as a comment.
  • Newest first — threads are numbered in creation order (oldest is #1) for a stable reference, then listed newest first.
  • Click to locate — a row selects its annotation and scrolls the page to it, where the popover opens the full thread.
  • Follows the viewer — with no props it lists the ambient viewer document's comments. Pass documentId to pin it to a specific document.

Author names and avatars come from the nearest <PdfCommentUserProvider>; without one, everyone shows coloured initials.

Annotation is a heavy-tier plugin, so <PdfViewer /> doesn't register it for you. The plugin and <PdfCommentLayer /> both have to be on the viewer before the panel shows anything — the layer draws the pin a row navigates to, and the thread that opens on it.

Installation

pnpm dlx shadcn@latest add @pdfviewcn/comment-sidebar

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. `annotationAuthor` attributes pins and replies.
const plugins = [
  createPluginRegistration(AnnotationPluginPackage, {
    annotationAuthor: "You",
  }),
];

Render the comment layer, a way to add pins, and the panel beside the content.

<PdfViewer
  documents={[{ url: "/sample.pdf" }]}
  plugins={plugins}
  className="h-[720px]"
>
  <PdfToolbar>
    <PdfCommentButton />
  </PdfToolbar>
  <div className="flex min-h-0 flex-1">
    <PdfViewerContent
      className="flex-1"
      pageLayers={({ documentId, pageIndex }) => (
        <PdfCommentLayer documentId={documentId} pageIndex={pageIndex} />
      )}
    />
    <PdfCommentSidebar className="w-72 border-l" />
  </div>
</PdfViewer>

The thread a row navigates to comes from <PdfCommentLayer />, so the layer isn't optional here — without it a row scrolls to a marker with nothing to open.

Wrap the viewer in <PdfCommentUserProvider> to set the current author and resolve avatars — match its currentUser.name to the plugin's annotationAuthor so a person's pins and replies read as the same author.

Examples

What counts as a comment

The panel lists conversations, not annotations. A thread appears once it holds at least one message — the root's own text, or any reply to it.

So a rectangle you just drew doesn't show up; it's artwork, and it belongs in the annotation sidebar. Neither does a pin you dropped but haven't typed into yet. What does show up is any pin with text, and any annotation somebody has replied to with <PdfCommentThreadButton />.

A thread hanging off artwork is labelled with what it's attached to — #3 · Page 2 · on highlight. Its avatar stack, author, time, and preview come from the first thing said in it, not from whoever drew the shape.

The rule is exported as isPdfCommentThread for panels of your own. See Model helpers.

Accessibility

The panel is a <ul> of real <button> rows, so the list is announced with its length and every thread is reachable with Tab. The focus ring is inset rather than outset, since an outset ring on the first or last row would be clipped by the scroll container.

A row carries aria-current while its annotation is selected, so the tint that marks the on-page selection is announced rather than only seen. Selection is read from plugin state, so picking a pin on the page moves the mark here too.

Everything in a row sits inside its button — participants, the #3 · Page 2 reference, author, time, preview, reply count — so one stop reads the whole thread. The preview is clamped to three lines with CSS, which shortens what's shown without shortening what's read, and the author's name is printed as text beside the avatar stack, so nothing depends on the avatars resolving.

An empty panel says so, with what to do about it.

API Reference

PdfCommentSidebar

A <div> carrying data-slot="pdf-comment-sidebar". Accepts every prop a <div> takes, plus:

PropTypeDefaultDescription
documentIdstring | nullambient documentThe document whose comments to list. Defaults to the focused view's active document; pass an id to pin it to one.
headingReact.ReactNode"Comments"The panel's title, shown beside a thread count. null drops the header entirely.

Returns null when the document isn't loaded or the annotation plugin isn't registered. Each row is a button carrying data-selected when its annotation is selected. The panel never mutates annotations, so it takes no permission gate; replying happens in the on-page <PdfCommentPopover />, which respects the document's permissions.

Pass heading={null} when the surrounding chrome already names the panel — the annotator app puts both sidebars behind a tab strip and does exactly that.

The header carries data-slot="pdf-comment-sidebar-header".