Comment Margin
Comment cards beside the text they are about — level with their highlight, stacked without overlap, the active one aligned — and a popover for narrow screens.
- Status
- Since
0.23.74- Accessibility pattern
- plain column of cards; popover dialog with Escape
Last updated
Is 25 A right for a 7 kW cooker?
Add a photo of the earth bar.
Who signs off the RCD test?
Label the new circuit.
Install
Add Comment Margin from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/comment-marginUsage
import { CommentMargin } from "@/components/ui/comment-margin";
<CommentMargin
activeId={activeId}
items={layout.map((item) => ({ id: item.id, top: item.top, node: <CommentThread … /> }))}
/>;Put the margin in a column beside the editor with its top level with the TextEdit root: each
item's top is then TextEdit's onAnnotationsLayout top as it comes. A card sits level with
its highlight and is pushed down just enough not to overlap the card above it (gap, 12px). The
active card sits exactly level with its text and the cards above it move up to make room — Google
Docs' behaviour. Heights are measured live, so a card that grows (its reply box opening) re-flows
the rest. A card whose top is null — its text was deleted — is not drawn; list those threads
elsewhere ("Text removed").
Is 25 A right for a 7 kW cooker?
Add a photo of the earth bar.
Who signs off the RCD test?
Label the new circuit.
Scope
- Owns: the stacking of cards beside their text, and the popover for narrow screens.
- Does not own: the cards (compose
CommentThread), the highlights or their positions (TextEdit). - Compose with:
TextEdit'sannotationsandonAnnotationsLayout,CommentThread, andSheet side="bottom"on touch-sized screens.
Anatomy
Examples
Popover below the margin breakpoint
Where no margin fits, open the thread in a CommentPopover anchored to the highlight's box — read
it with getBoundingClientRect() on the [data-annotation] element. On touch-sized screens use
Sheet side="bottom" instead.
layoutCommentMargin(items, gap, activeId) is the pure layout the margin runs, exported for hosts
that draw their own column.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* | readonly CommentMarginItem[] | — | The cards, each with its desired top. |
activeId | string | null | The active card sits level with its text: earlier cards move up to make room for it. |
className | string | — | Classes for the margin column. |
gap | number | 12 | The space kept between two cards, in px. |
Data attributes and CSS variables on CommentMargin
| Attribute | Values |
|---|---|
data-active | "" |
data-slot | "comment-margin" | "comment-margin-item" |
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | Stable id — the annotation / thread id. |
node* | React.ReactNode | — | The card — a CommentThread, usually. |
top* | number | null | — | The desired top edge in px, in the margin's own coordinates — TextEdit's
onAnnotationsLayout top when the margin's top lines up with the editor root's. null
(orphaned) is not rendered: list those threads elsewhere. |
| Prop | Type | Default | Description |
|---|---|---|---|
anchorRect* | DOMRect | null | — | The highlight's box in viewport coordinates — the popover anchors to it. null closes it. |
children* | React.ReactNode | — | The content — a CommentThread, usually. |
onOpenChange* | (open: boolean) => void | — | Called when it opens or closes (Escape, a click outside). |
open* | boolean | — | Whether it shows. |
className | string | — | Classes for the popup. |
Data attributes and CSS variables on CommentPopover
| Attribute | Values |
|---|---|
data-slot | "comment-popover" |
Accessibility
- The margin is a plain column: each card brings its own semantics (
CommentThreadis anarticle). Cards follow their highlights' order in the DOM, so reading order matches the page. CommentPopoveris a Base UI popover named "Comment thread": focus moves into it, Esc closes it and focus returns.
| Key | Action |
|---|---|
| Tab | Move through the cards' controls |
| Esc | Close the popover (onOpenChange(false)) |
| Contract | States tested |
|---|---|
| Behaviour | stacked, active-aligned, orphan-skipped, popover |
| Accessibility | browser-accessibility-test, dismissable |
| Visual | default, active |
Do / Don't
Comments
A record's comments — a list with a count, an Oldest or Newest first toggle, skeleton and empty states, in-place editing, and a Linear-style composer.
Version List
A document's version history — time, author, name, kind badges (Current, Restored, Unsaved copy), a Named only switch and Load more — as a keyboard listbox.