- 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 Draft
- PDF Comment Button
- PDF Comment Sidebar
- PDF Redaction Toolbar
- PDF Capture Button
- PDF Form Fill Toggle
- PDF Signature Button
- PDF Stamp Button
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 Nreference, 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
documentIdto 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.
The panel renders nothing at all when the annotation plugin isn't registered. If it stays empty, that's the missing registration rather than a document with no comments.
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:
| Prop | Type | Default | Description |
|---|---|---|---|
documentId | string | null | ambient document | The document whose comments to list. Defaults to the focused view's active document; pass an id to pin it to one. |
heading | React.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".