> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pins, Bubbles & Tools

> Reference runtime behavior for comment pins, bubbles, and tool surfaces.

`VeltCommentPin`, `VeltCommentBubble`, and `VeltCommentTool` are the three **anchored / Velt-positioned** comment surfaces: they drop where the user clicks, attach to a target element, or sit inside Velt's own comment host, and **Velt owns their placement**. This file owns the layer above [`Props`](https://velt.dev/docs/ui-customization/reference/props) for these three: **what every prop does at runtime, its default, and how it combines with the others**, plus the default click/hover/drag/index behaviors you get with no prop at all.

* **Who positions them:** see [`Behaviors` → Positioning ownership](https://velt.dev/docs/ui-customization/reference/behaviors#positioning-ownership-anchored-surfaces). Pins drop where clicked, the dialog anchors to its pin and re-anchors on resize, bubbles attach to their target element: all for free **as long as you stay in wireframes**. Drop to primitives/headless and you take over positioning.
* **Anchored-vs-static classification:** see [`Component definitions`](https://velt.dev/docs/ui-customization/reference/component-definitions). Pins, the pin dialog, and bubbles are anchored; the sidebar, inline comments section, and notifications panel are statically placed.

These three are siblings of the same comment-annotation model: a **tool** starts a comment, a **pin** marks where it landed, a **bubble** is the pin's alternate "chip on an element" rendering. They share the `context` / `contextOptions` / `locationId` / `documentId` / `folderId` scoping props with identical semantics, documented once here per component.

***

## Per-prop behavior (exhaustive)

The exhaustive per-prop behavior for the three anchored comment surfaces is organized **per component** below: each `## Velt…` subsection carries that component's full prop table (default + runtime behavior + interactions) and its own `### Default behaviors (no prop needed)` block. Every prop in [`Props`](https://velt.dev/docs/ui-customization/reference/props) for these components is covered.

## VeltCommentPin

The pin marks an existing comment annotation's anchor point and opens its dialog. In React, `VeltCommentPin` renders **one** pin for a known annotation (you pass `annotationId`); the auto-managed pins that `VeltComments` drops on click are the same underlying element driven by the host. Pin placement, drag, dialog open/close, and the index/number badge are **default behaviors** (global feature state), not per-instance props: see *Default behaviors* below.

| Prop | Default | Behavior | Interactions & gotchas |
| - | - | - | - |
| `annotationId` | (none) | The annotation this pin represents. The pin reads that annotation's resolved anchor, comments, author, status, and unread state, and binds its dialog to it. | Required for a standalone `VeltCommentPin`; the host-managed pins set it internally. With no `annotationId` (and no other scope) the pin has no annotation to render. Changing it re-syncs the pin to the new annotation. |
| `multiThreadAnnotationId` | (none) | Selects one thread **within** a multi-thread pin (several comment threads sharing one anchor). Scopes the dialog to that thread. | Only meaningful when the annotation is a multi-thread pin; ignored otherwise. Pairs with multi-thread dialog primitives. |
| `variant` | (none → base wireframe) | Picks the named pin **wireframe variant** to render. Resolves the registered wireframe `velt-comment-pin-wireframe---<variant>`; if none is registered for that name, the pin falls back to the base wireframe. | The variant string is also the scoping key: a variant only renders content registered under that exact name. Use to render different pin looks in different contexts (see [`Behaviors` → Variant → context scoping](https://velt.dev/docs/ui-customization/reference/behaviors#variant--context-scoping)). |
| `context` | (none) | Free-form key/value object attached to **new** comments created via this pin's flow, and used to scope which annotation the pin resolves. | Stringified to the element attribute internally; pass an object. Combines with `contextOptions` (resolution rules) and the `locationId`/`documentId`/`folderId` scope. Does **not** retro-tag existing comments. |
| `contextOptions` | (none) | Resolution options governing how `context` is matched when locating/creating the annotation. | Only has effect alongside `context`. |
| `locationId` | (none) | Scopes the pin to a specific Velt **location** (sub-page / tab / canvas). | Considered only when a `folderId` or `documentId` is also in play; a bare `locationId` does not narrow resolution on its own. |
| `documentId` | (none) | Scopes the pin to a specific document. The client id you pass is converted to the internal server document id. | Falsy value clears it to `undefined` (not `null`) for hash parity with the tool/bubble. |
| `folderId` | (none) | Scopes the pin to a folder; the server folder id is derived from it. | Same undefined-on-clear parity as `documentId`. |
| `defaultCondition` | (unset → pin shows) | Render gate. `true` forces the pin to render; `false` hides it. Passed through as the `default-condition` attribute and consumed by Velt's conditional-rendering layer. | Tri-state: only literal `true`/`false` emit the attribute: any other value leaves it unset (default render). Use to conditionally suppress a pin without unmounting the component. |

### Default behaviors (no prop needed)

* **Pin placement (anchored):** the pin is positioned by the **host** at the annotation's resolved anchor: where the user clicked, or on the target element. The pin element itself takes no `x`/`y`/`position` input. Default pin marker size is 40px (34px when the comment was added on a different device type and the device indicator is on).
* **Click opens the dialog:** pointer-up on the pin toggles selection. If the pin is **not** selected, the click selects it and opens its anchored dialog; if already selected, it toggles closed. This is `togglePinSelection`, emitted on pointer-up. (Pointer-up on the dialog itself also selects the pin if it wasn't already, so interacting with the dialog keeps it open.)
* **Drag to reposition:** pins are **draggable by default** (`pinDrag` defaults `true` in global feature state). Dragging sets an `annotationDragging` state and, on drop, re-anchors the annotation. There is no per-pin prop to disable drag on `VeltCommentPin`; it is governed by the global comment feature config.
* **Hover / bubble-on-pin:** `bubbleOnPin` and `bubbleOnPinHover` (global feature state, both default `false`) control whether a preview bubble shows on the pin permanently or on hover. Off by default: the bare pin shows the avatar marker only.
* **Index / number badge:** `showCommentIndex` (global, default `false`) toggles a numeric index badge on pins (the `comment-pin-number` / `comment-pin-index` primitives). Off by default, so pins show the author avatar, not a number. Unread, private, ghost, and agent-comment states each swap in their own marker primitive (agent comments render a distinct blue lego-bot marker instead of an avatar).
* **Shadow DOM:** the pin's own shadow-DOM isolation comes from global `pinShadowDOM` (default `false`), **not** from a per-pin prop: `VeltCommentPin` exposes no `shadowDom` prop. (Confirmed: the pin **marker** defaults `pinShadowDOM: false`, while the pin **dialog** defaults `dialogShadowDom: true`: the two surfaces isolate independently; see [`Behaviors`](https://velt.dev/docs/ui-customization/reference/behaviors).)

***

## VeltCommentBubble

The bubble is a chip that attaches to a target element (or an annotation) and, on click, opens that annotation's comment dialog. It is the "comment count on a row/card" surface. Unlike the pin, the bubble **fetches its own annotation** from the scope you give it (`targetElementId` / `annotationId` / context+scope).

| Prop | Default | Behavior | Interactions & gotchas |
| - | - | - | - |
| `targetElementId` | (none) | The element id the bubble attaches to; the bubble resolves the annotation(s) bound to that element and anchors to it. | Triggers an annotation fetch on set/change. Primary way to place a bubble on a list row or card. |
| `targetCommentElementId` | (none) | **Deprecated alias for `targetElementId`.** Setting it writes through to `targetElementId`. | Use `targetElementId`. Kept only for back-compat; identical behavior. |
| `annotationId` | (none) | Binds the bubble to one specific annotation instead of resolving by element/scope. | Triggers a fetch on change. Takes precedence as a direct bind; combine with scope props only when you have no fixed id. |
| `avatar` | `true` (shows) | Shows the commenter avatar on the bubble. `false` hides it. | Writes to the same `showAvatar` UI state as the deprecated `showAvatar` prop: last setter wins. |
| `showAvatar` | `true` (shows) | **Deprecated alias for `avatar`.** | Use `avatar`. Both target one flag; passing conflicting values means the later-applied one wins. |
| `commentCountType` | `'total'` | Which count the bubble renders: `'total'` = all comments on the annotation; `'unread'` = unread count only. | Enum-backed (`CommentCountType.TOTAL`/`UNREAD`). The count primitive branches on this value; with `'unread'` and nothing unread, the count reflects zero/none. |
| `commentBubbleTargetPinHover` | `false` | When `true`, hovering the bubble highlights its target pin/element (adds a hover class on the linked target). | Off by default: hovering does nothing visible. Use to cross-highlight a bubble and its anchor in dense layouts. |
| `openDialog` | `true` | Whether clicking the bubble opens the comment dialog. `true` → click toggles the anchored dialog open/closed. `false` → click does **not** open a dialog; instead it only emits the `commentBubbleClick` event. | The escape hatch for "bubble as a pure click target": set `openDialog={false}` and listen for the click event to drive your own UI. With `true`, click runs the full select/toggle flow (resets other selections, scrolls to drafts, toggles this annotation). |
| `readOnly` | `false` | Renders the bubble's dialog view-only (no composing/editing). | OR-combined with the global `readOnly` feature state: if either is on, the bubble is read-only. |
| `variant` | (none → base wireframe) | Picks the named bubble **wireframe variant** (`velt-comment-bubble-wireframe---<variant>`), else base. | Same variant-as-scoping-key rule as the pin. |
| `darkMode` | `false` | Forces the dark theme on the bubble; sets a `dark` attribute on the host. | Passing `null`/`undefined` leaves the theme unmanaged (inherits ambient). String `"false"` is treated as off. |
| `shadowDom` | `true` | Style-isolates the bubble in shadow DOM. `false` exposes it to selector CSS. | The component tracks whether `true` was **explicitly** passed (vs defaulting): relevant when a complete consumer wireframe is provided, which otherwise auto-wraps. Set `false` to style with plain CSS classes. |
| `context` | (none) | Key/value object attached to **new** comments created through the bubble and used to scope annotation resolution. | Stringified internally; pass an object. Re-triggers the annotation fetch on change. Does not retro-tag existing comments. |
| `contextOptions` | (none) | Resolution options for how `context` is matched. | Effective only with `context`. |
| `locationId` | (none) | Scopes the bubble to a Velt location; re-triggers fetch. | Behaves with `folderId`/`documentId` as the resolution narrows. |
| `documentId` | (none) | Scopes to a document (client id → server document id); re-triggers fetch. | Falsy clears to `undefined`. |
| `folderId` | (none) | Scopes to a folder (server folder id derived); re-triggers fetch. | Falsy clears to `undefined`. |

### Default behaviors (no prop needed)

* **Anchored placement:** the bubble attaches to its `targetElementId` and is positioned by Velt relative to that element; you don't pass coordinates.
* **Click → dialog (default on):** with default `openDialog={true}`, clicking the bubble selects the annotation and opens its dialog, resetting any other selected annotation first and scrolling drafts into view. Clicking again toggles it closed.
* **Click event always fires:** the `commentBubbleClick` event is emitted on every click **regardless** of `openDialog`, so even `openDialog={false}` bubbles are usable as click targets.
* **Count + avatar shown:** by default the bubble shows the author avatar and the **total** comment count, plus an unread indicator when there are unread comments.
* **Shadow-DOM wrapped:** default `shadowDom={true}` isolates styles; if you register a full bubble wireframe and don't pass shadow DOM, the root still resolves its own wireframe so it doesn't auto-wrap unexpectedly.

***

## VeltCommentTool

The tool is the button that **starts** a new comment. Clicking it either enters "click anywhere to place a pin" add-comment mode, attaches a comment directly to a target element (popover mode), or opens the sidebar's page-mode composer: depending on configuration. It carries no avatar/count; its job is to begin the flow.

| Prop | Default | Behavior | Interactions & gotchas |
| - | - | - | - |
| `targetElementId` | (none) | When set, clicking the tool attaches the new comment **directly to that element** (in popover mode) instead of entering free placement mode. | Triggers an annotation-availability check. If the target already has a (visible) comment annotation, the tool hides itself (see default behaviors). |
| `targetCommentElementId` | (none) | **Deprecated alias for `targetElementId`.** Writes through to it. | Use `targetElementId`. |
| `onCommentModeChange` | (no handler) | Callback fired whenever add-comment mode toggles on/off; receives the boolean mode. | Fires on every transition (also when mode is toggled elsewhere, since it's driven by the shared add-comment-mode stream). Use to sync your own UI (e.g. button pressed state). |
| `sourceId` | (none) | Identifier stamped on the comment when the tool attaches it to a target element. | Only passed through in the direct-attach (popover + target) path. |
| `darkMode` | `false` | Forces the dark theme on the tool. | String `"false"` treated as off. |
| `disabled` | `false` | Disables the tool. When `true`, clicking the tool does nothing (add-comment is blocked). | Combined with the **global** comment-tool-enabled state via AND: the tool is active only when not `disabled` **and** the global tool is enabled. Independent of IAM permission checks (below), which can also block the action. |
| `variant` | (none → base wireframe) | Picks the named tool **wireframe variant** (`velt-comment-tool-wireframe---<variant>`), else base. | Same variant-as-scoping-key rule as pin/bubble. |
| `shadowDom` | `true` | Style-isolates the tool in shadow DOM. `false` exposes it to selector CSS. | Tracks explicit-`true` like the bubble, so providing a full custom button slot doesn't get force-wrapped. |
| `context` | (none) | Key/value object attached to the **new** comment the tool creates; in free placement mode it is set as the free-style comment context, in direct-attach mode it is passed to the attach call. | Stringified internally; pass an object. Re-checks annotation availability on change. |
| `contextOptions` | (none) | Resolution options for `context`. | When `contextInPageModeComposer` is on, both `context` and `contextOptions` are **forced to undefined** for the availability subscription (the page-mode composer owns its own context). |
| `locationId` | (none) | Scopes the new comment to a Velt location; used to resolve/attach. | Considered for availability only when a `folderId` or `documentId` is also set; supplied as a manual location when attaching directly. |
| `documentId` | (none) | Scopes to a document (client id → server document id). | Falsy clears to `undefined` (hash parity with bubble). Becomes attach metadata in direct-attach mode. |
| `folderId` | (none) | Scopes to a folder (server folder id derived). | Falsy clears to `undefined`. Becomes attach metadata in direct-attach mode. |
| `contextInPageModeComposer` | `false` | When `true`, clicking the tool routes the `context` + `targetElementId` into the **sidebar page-mode composer** and opens the sidebar, instead of placing a pin or attaching inline. | Can also be turned on globally via API: the click honors either the prop **or** the global flag. While on, it suppresses context-scoping of the tool's own availability subscription (see `contextOptions`). Use to make a tool feed a sidebar composer rather than drop a pin. |

### Default behaviors (no prop needed)

* **Default button when no custom slot:** if you don't fill the tool's button slot, Velt renders its **default comment-tool button**. Provide your own element in the slot to replace it (the tool detects an empty slot and shows the default only then).
* **Click → add-comment mode (default):** with no `targetElementId`/popover config, clicking the tool **enables add-comment mode**: the next click on the page drops a pin. Clicking the tool again while in add-comment mode **disables** it (toggle). The host adds a `comment-tool-active` class while mode is on.
* **Direct attach in popover mode:** when popover mode is on **and** a `targetElementId`/`context`/scope is provided, the click attaches the comment straight to that element (carrying `sourceId` and folder/document/location metadata) rather than entering placement mode.
* **Auto-hide when the target already has a comment:** in popover mode, the tool subscribes to annotation availability for its target; if a (visible, unresolved unless resolved-on-DOM is on) comment already exists there, `commentAnnotationAvailable` becomes true and the tool hides, so you don't stack a second tool over an existing comment.
* **Permission & auth gates:** the click is a no-op (with a console note) for anonymous/read-only users, when the SDK isn't authenticated, when the plan is expired, or when the user lacks an ADMIN/COMMENTER role. These gates are in addition to the `disabled` prop and the global tool-enabled flag.
* **Analytics:** every tool click emits a `COMMENT_TOOL_CLICKED` analytics event tagged `custom` or `default` depending on whether a custom button slot was supplied.

***

## Default behaviors (no prop needed)

Each component's zero-prop baseline is documented in its `### Default behaviors (no prop needed)` block above. In brief:

* **Pin**: placed by the host at the annotation's resolved anchor (no `x`/`y` input), opens its dialog on click, draggable by default (`pinDrag` global, `true`), shows the author avatar (no index/bubble unless the global flags are on), isolates via global `pinShadowDOM` (default `false`).
* **Bubble**: anchors to its `targetElementId`, opens the dialog on click (default `openDialog={true}`) while always emitting `commentBubbleClick`, shows the avatar + total count, shadow-DOM wrapped.
* **Tool**: renders Velt's default button (unless you fill its slot), enters add-comment mode on click (or direct-attaches in popover mode), auto-hides when its target already has a comment, gated by auth/permission checks plus the global tool-enabled flag.

## Prop-interaction matrix

No separate multi-prop matrix for this group: pairwise effects live in each prop's **Interactions & gotchas** column above. The recurring cross-prop notes: the scoping set (`context` + `contextOptions` + `locationId` + `documentId` + `folderId`) resolves together, and `locationId` only narrows when a `folderId`/`documentId` is also present; the deprecated aliases (`showAvatar`→`avatar`, `targetCommentElementId`→`targetElementId`) write the same flag as their replacement (last setter wins); `disabled` on the tool AND-combines with the global tool-enabled state; and `contextInPageModeComposer` forces the tool's own `context`/`contextOptions` to `undefined` for its availability subscription. Pin/bubble/dialog shadow-DOM isolation is covered in the [behaviors index](https://velt.dev/docs/ui-customization/reference/behaviors.md).

## Positioning & composition

All three surfaces are **anchored / Velt-positioned**: the pin drops where the user clicked (or on its target element), the bubble attaches to its `targetElementId`, and the tool/its resulting pin are placed by Velt's comment host: none take a coordinate input. You keep this anchoring **for free while you stay in wireframes**; dropping to primitives or headless means you take over positioning. See [`Behaviors` → Positioning ownership](https://velt.dev/docs/ui-customization/reference/behaviors#positioning-ownership-anchored-surfaces) and the anchored-vs-static classification in [`Component definitions`](https://velt.dev/docs/ui-customization/reference/component-definitions).

Every documented prop of these components is covered above. Note that `VeltCommentPin` exposes no `shadowDom` prop of its own: pin shadow-DOM isolation is controlled by the global `pinShadowDom` flag on `<VeltComments>`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.