Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
Since
0.23.74
Accessibility pattern
plain column of cards; popover dialog with Escape

Last updated

Highlight A at 0px
Highlight B at 16px
Highlight C at 40px
Highlight D at 260px

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-margin

Usage

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").

Highlight A at 0px
Highlight B at 16px
Highlight C at 40px
Highlight D at 260px

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's annotations and onAnnotationsLayout, CommentThread, and Sheet side="bottom" on touch-sized screens.

Anatomy

CommentMargin — data-slot="comment-margin" | "comment-margin-item"
CommentPopover — data-slot="comment-popover"

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

PropTypeDefaultDescription
items*readonly CommentMarginItem[]—The cards, each with its desired top.
activeIdstringnullThe active card sits level with its text: earlier cards move up to make room for it.
classNamestring—Classes for the margin column.
gapnumber12The space kept between two cards, in px.

Data attributes and CSS variables on CommentMargin

AttributeValues
data-active""
data-slot"comment-margin" | "comment-margin-item"
PropTypeDefaultDescription
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.
PropTypeDefaultDescription
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.
classNamestring—Classes for the popup.

Data attributes and CSS variables on CommentPopover

AttributeValues
data-slot"comment-popover"

Accessibility

  • The margin is a plain column: each card brings its own semantics (CommentThread is an article). Cards follow their highlights' order in the DOM, so reading order matches the page.
  • CommentPopover is a Base UI popover named "Comment thread": focus moves into it, Esc closes it and focus returns.
KeyAction
TabMove through the cards' controls
EscClose the popover (onOpenChange(false))
ContractStates tested
Behaviourstacked, active-aligned, orphan-skipped, popover
Accessibilitybrowser-accessibility-test, dismissable
Visualdefault, active

Do / Don't

Do
Feed the margin straight from onAnnotationsLayout, and make the clicked highlight's thread active.
Don't
Position cards yourself with the highlight's offset, or render orphaned threads in the margin.

On this page