Skip to main content
What it is: Velt’s building-block components that you compose yourself. Each one comes with Velt’s default design and full behavior, and there’s a sub-component for nearly every child piece. You fetch the data, loop it, pass the props, arrange the pieces into any layout, wrap them in any UI library, and add your own interactivity. Most control, most effort. Use it when (any of these): you need your own UI component library inside the comment UI, your own interactivity/state, to place Velt pieces anywhere in your tree, to compose layouts beyond what wireframe slots offer, or to customize a leaf piece (via that leaf’s wireframe). (Decision tree Q3.) Don’t use it when: wireframe slots already express the layout you need: wireframes are less work because Velt does the data-fetching/looping/wiring for you. Reach for primitives only when you need the extra control. (And for pure theming, just use CSS.)

The model

You do the composition. A primitive is a normal React component that renders a Velt custom element (<velt-…>). Examples: VeltComments, VeltCommentDialog, VeltCommentsSidebar, VeltCommentBubble, VeltCommentText, VeltNotificationsPanel, VeltReactionTool: plus a sub-component for nearly every child. The trade vs. wireframes: you write the React that Velt would otherwise do for you. To show all threads you fetch the annotations, .map() them, and pass each annotationId into a VeltCommentDialog; to render a thread you take the comments array, loop it, and pass each comment into a thread-card primitive; you handle conditional show/hide yourself. That’s more work, and in return you get full layout control, your UI library, and real interactivity.
Primitives vs. wireframes: two parallel APIs, not layers of each other, and you can combine them on one surface. A primitive renders Velt’s default UI (which you compose); a wireframe supplies layout markup that Velt fills. Deep layout control: because there’s a sub-component for nearly every child, you can rebuild a piece’s layout by composing its sub-components (e.g. a custom header from the header’s sub-components). The one limit: leaf primitives (deepest pieces, no children) can’t be restructured as primitives: to customize a leaf, use that leaf’s wireframe (even inside an otherwise-primitive build).
Component and sub-component names: Component catalog. Layout/mode props: Component config.

Steps

1

Drop in the component

import { VeltComments, VeltCommentsSidebar } from '@veltdev/react';

<VeltComments shadowDom={false} />
<VeltCommentsSidebar shadowDom={false} />
That alone gives you fully working comments with Velt’s default design. (Keep shadowDom={false} if you’ll style them: see CSS.)
2

Toggle features with props

Trim the UI to your design by switching features off: never by hiding them with CSS. These are all real VeltComments props:
<VeltComments
  shadowDom={false}
  userMentions={true}
  reactions={false}
  recordings="none"
  attachments={false}
  status={false}
  priority={false}
  commentTool={false}
  collapsedComments={true}
  commentPlaceholder="Add a comment…"
  replyPlaceholder="Reply…"
/>
All <VeltComments> props are in Props; layout/mode props for the sidebar/dialog/notifications are in Component config. Don’t guess prop names.
3

Build a real comment UI: fetch → loop → render

VeltComments renders the whole default comments experience for you. But when you want your own layout from primitives, you do the data plumbing yourself: fetch the annotations with a hook, loop them, and render a dialog/thread primitive per annotation by passing its annotationId. That’s the actual “primitive implementation”:
import { VeltCommentDialog, useCommentAnnotations, useVeltInitState } from '@veltdev/react';
import type { CommentAnnotation } from '@veltdev/types';

function MyCommentsList() {
  const ready = useVeltInitState();
  // 1) FETCH: reactive; re-renders when comments change (incl. other users live)
  const annotations: CommentAnnotation[] = useCommentAnnotations() ?? [];

  if (!ready) return <MySkeleton />;
  if (annotations.length === 0) return <MyEmptyState />;

  return (
    <div className="my-list">
      {/* 2) LOOP: one row per annotation */}
      {annotations.map((a) => (
        <section key={a.annotationId} className="my-row">
          <header>{a.comments?.[0]?.from?.name}</header>
          {/* 3) RENDER a Velt primitive, scoped by annotation-id: Velt fills in
                 the thread (comments, composer, reactions, resolve, …) with full behavior.
                 defaultCondition skips the internal show/hide gate so YOU control visibility. */}
          <VeltCommentDialog annotationId={a.annotationId} fullExpanded defaultCondition={false} />
        </section>
      ))}
    </div>
  );
}
Use VeltCommentDialog, not VeltCommentThread. VeltCommentThread is deprecated: VeltCommentDialog is the supported per-thread primitive (same annotationId API). Don’t reach for VeltCommentThread in new code.
What’s happening, and why it’s different from <VeltComments>:
  • useCommentAnnotations() is the data source: a reactive array of CommentAnnotation (Hooks). You can filter/sort/group it however your design needs before rendering.
  • You .map() it and render a primitive per item. VeltCommentDialog takes an annotationId and renders that thread’s full UI + behavior: you’re not re-implementing comments, you’re placing Velt’s thread inside your row layout. (defaultCondition={false} opts out of the primitive’s internal visibility gate: see the default-condition note below.)
  • This is exactly the trade vs. wireframes: wireframes do the fetch+loop for you (you only supply slot markup); primitives make you write the fetch+loop, in exchange for full control over the surrounding structure and the freedom to wrap each row in your UI library.
  • To create a thread from your own composer, pair this with the action hooks (useAddCommentAnnotation, useAddComment: see headless, which is the same data layer taken all the way).
Single known thread? You can skip the loop and render one <VeltCommentDialog annotationId={id} fullExpanded defaultCondition={false} /> directly.

default-condition: you control show/hide (primitives only)

Every primitive has an internal visibility condition: it decides on its own whether to render (e.g. a VeltCommentDialog shows only when its annotation is “selected”/open). The defaultCondition prop lets you bypass that internal gate and force the component to render regardless, so you own the show/hide logic from your side:
{/* Render this thread no matter Velt's internal selected/open state: you decide when to mount it */}
<VeltCommentDialog annotationId={a.annotationId} defaultCondition={false} fullExpanded />
  • defaultCondition={false} = skip the internal condition → the component renders whenever you mount it (you gate it with your own {show && <…/>}, routing, tabs, etc.).
  • Omit it = the primitive uses its own built-in condition (default behavior).
This is essential for the fetch→loop→render pattern above (you’re deliberately rendering a dialog per row, so you don’t want each one waiting to be “selected”).
Wireframes do NOT have this. A wireframe always renders through Velt’s internal condition: there is no defaultCondition escape hatch. If your design needs to force-show/force-hide a piece on your own logic, that’s a reason to use a primitive, not a wireframe. (In a wireframe you can only react to Velt’s state via velt-if on the exposed variables: you can’t override whether Velt decides to render the component at all.)
4

Wrap in your UI library

Because primitives are real components, your design-system components work normally around them:
// MUI, shadcn, Radix, Ant, Chakra, Tailwind: all fine
<Card className="my-thread-card">
  <CardHeader title="Discussion" />
  <CardContent>
    <VeltCommentDialog annotationId={annotationId} fullExpanded />
  </CardContent>
</Card>
Your <Card> keeps its own state, click handlers, and styling. Velt renders the dialog inside it. This is exactly what wireframes cannot do (their slot markup is cloned, so interactive components inside them go dead: see Wireframes and Edge cases and limitations).
5

Theme with CSS

Match colors/spacing to your brand via --velt-* variables: see CSS.

What it can and can’t do

✅ Primitives can❌ Primitives can’t
Render fully working Velt UIRestructure a leaf piece (no children): use that leaf’s wireframe instead
Restructure layout by composing sub-component primitives (custom header, custom thread-card, etc.)Save you from writing the composition (you fetch/loop/pass props yourself)
Be composed inside your own UI library, with your own interactivity/stateNone
Be placed anywhere in your treeNone
Toggle features via props; be themed with CSSNone
Primitives do give deep layout control (via sub-components). The two reasons to not use them: (1) a wireframe already expresses the layout with less work, or (2) you only need to restructure a single leaf: do that with the leaf’s wireframe.

Notes & deep-dives

Worked example: a custom dialog header with a custom dropdown (your UI library + Velt items)

A very common need: you build your own comment dialog from primitives, and in the header you want status / priority / options dropdowns that use your own UI-library dropdown (Radix/MUI/etc.) for the open/close shell, but still drive Velt’s real behavior, with wireframe-styled items. Here’s how the pieces fit. Velt ships a full primitive dropdown family for each (all real React components that carry behavior):
  • Status: VeltCommentDialogStatusDropdown (props annotationId, onChangeStatus, disabled) → …Trigger (…TriggerName / …TriggerIcon / …TriggerArrow) + …Content…ContentItem (props statusObj / statusId / statusIndex) → …ContentItemIcon / …ContentItemName.
  • Priority: VeltCommentDialogPriorityDropdown… (same shape; item also has …ContentItemTick).
  • Options: VeltCommentDialogOptionsDropdown…Trigger + …Content → individual action items …ContentEdit, …ContentDelete (…DeleteComment / …DeleteThread), …ContentAssign, …ContentMakePrivate (…Enable/…Disable), …ContentNotification (…Subscribe/…Unsubscribe), …ContentMarkAsRead.
You have three ways to make the dropdown “yours”: pick by how much of the shell you want to own: Option A: Velt dropdown shell, your item styling (least effort). Use the primitive dropdown as-is and restyle each item by registering its wireframe content (VeltCommentDialogStatusDropdownContentWireframe.Item.Icon / .Name). You keep Velt’s open/close + set-status behavior; items look fully custom. Option B: your UI-library dropdown shell + Velt primitive items (the case you asked about). Render your library’s dropdown. It can be interactive because primitives are real React; the wireframe cloning limit does not apply here. Put your trigger and popover from your library, and inside the popover render Velt’s primitive content items, which carry the click-to-set behavior. Keep them inside the Velt dropdown container so they get the annotation context, and use onChangeStatus to sync your trigger label:
// `statuses` is your status config: the same list you pass to the `customStatus` prop
//   (see reference/component-config.md → "Custom data")
<VeltCommentDialogStatusDropdown
  annotationId={annotationId}
  onChangeStatus={(e) => setMyLabel(e.status)}   // keep your UI-lib trigger in sync
>
  <VeltCommentDialogStatusDropdownTrigger>
    <MyLibraryDropdownButton>{myLabel}</MyLibraryDropdownButton>   {/* YOUR shell */}
  </VeltCommentDialogStatusDropdownTrigger>

  <VeltCommentDialogStatusDropdownContent>
    <MyLibraryMenu>                                                {/* YOUR popover */}
      {statuses.map((s, i) => (
        // Velt primitive item = carries the set-status behavior
        <VeltCommentDialogStatusDropdownContentItem
          key={s.id} annotationId={annotationId} statusObj={s} statusId={s.id} statusIndex={i}
        >
          <VeltCommentDialogStatusDropdownContentItemIcon />
          <VeltCommentDialogStatusDropdownContentItemName />
        </VeltCommentDialogStatusDropdownContentItem>
      ))}
    </MyLibraryMenu>
  </VeltCommentDialogStatusDropdownContent>
</VeltCommentDialogStatusDropdown>
To restyle the item internals (icon/name layout), register the item’s wireframe (VeltCommentDialogStatusDropdownContentWireframe.Item). That is the “primitive items + wireframe to style the items” combo. The same shape works for Priority and for the Options action items. Option C: fully headless dropdown (max control). Render entirely your own dropdown and set the value with hooks: useUpdateStatus(), useUpdatePriority(), and the options actions (useResolveCommentAnnotation, useDeleteComment, useAssignUser, …). Read the current value from the annotation. No Velt dropdown component at all. (See Headless.)
Rule of thumb: want Velt’s behavior with your looks → Option A/B; want to own everything → Option C. All three keep Velt’s data/sync intact.

Checklist

  • Used real components/props (from Component catalog + Component config).
  • Features trimmed with props, not display:none.
  • shadowDom={false} if styling.
  • Your UI-library wrappers go around the primitive (not custom interactivity inside a wireframe).