0

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 render flush into the panel, no card or bubble, sourced straight from plugin state through getPdfCommentThreads, so the list always mirrors what's on the page.

  • A preview per thread — each row shows the thread's participants as an avatar stack, a #N · Page N reference, the author, relative time, the first few lines, and a reply count. It's an index entry, not the thread itself.
  • Where a thread landed — a row wears its review status once someone has ruled on it, so an unreviewed list stays quiet and a triaged one reads at a glance. 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, the way Figma indexes comments.
  • Click to locate — selecting a row selects its annotation and scrolls the page to it, where the on-page popover opens the full thread for reading and replying.
  • Follows the viewer — with no props it lists the ambient viewer document's comments. Pass documentId to pin it to a specific document.

Reading, replying, editing, and deleting all happen in the on-page thread, not here — the panel stays a lightweight, scannable index. Author names and avatars come from the nearest <PdfCommentUserProvider>; without one, everyone shows coloured initials.

Comment threads are part of the annotation plugin, which is a heavy-tier plugin <PdfViewer /> does not register for you. The plugin and the annotation layer both have to be added before the panel shows anything.

Installation

pnpm dlx shadcn@latest add pdfcn/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 annotation layer, a way to add pins, the on-page thread, 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 }) => (
        <PdfAnnotationLayer
          documentId={documentId}
          pageIndex={pageIndex}
          selectionMenu={(props) =>
            isPdfCommentPin(props.context.annotation.object) ? (
              <PdfCommentPopover documentId={documentId} {...props} />
            ) : null
          }
        />
      )}
    />
    <PdfCommentSidebar className="w-72 border-l" />
  </div>
</PdfViewer>

Clicking a row selects its pin and scrolls to it; the <PdfCommentPopover /> on the page is what opens for reading and replying. 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. The test is whether anyone has actually spoken: a thread appears once it holds at least one message, which is 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, the way Figma doesn't index a comment until it's posted. 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 — so the conversation stays tied to its subject without pretending the highlight is a comment. Its avatar stack, author, time, and preview all come from the first thing said in it, not from whoever drew the shape; someone who hasn't spoken isn't a participant.

That rule is exported as isPdfCommentThread, so a panel of your own can apply the same one. 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 rather than making a person tab through its parts. The preview is clamped to three lines with CSS, which shortens what's shown without shortening what's read. 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, instead of rendering nothing.

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 a data-selected attribute when its annotation is selected. The panel is read-only navigation — it never mutates annotations, so it needs no permission gate; reading and replying happen 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".