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
Drop in the component
shadowDom={false} if you’ll style them: see CSS.)Toggle features with props
Trim the UI to your design by switching features off: never by hiding them with CSS. These are all real All
VeltComments props:<VeltComments> props are in Props; layout/mode props for the sidebar/dialog/notifications are in Component config. Don’t guess prop names.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”:UseWhat’s happening, and why it’s different fromVeltCommentDialog, notVeltCommentThread.VeltCommentThreadis deprecated:VeltCommentDialogis the supported per-thread primitive (sameannotationIdAPI). Don’t reach forVeltCommentThreadin new code.
<VeltComments>:useCommentAnnotations()is the data source: a reactive array ofCommentAnnotation(Hooks). You can filter/sort/group it however your design needs before rendering.- You
.map()it and render a primitive per item.VeltCommentDialogtakes anannotationIdand 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 thedefault-conditionnote 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: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).
Wrap in your UI library
Because primitives are real components, your design-system components work normally around them: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).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 UI | Restructure 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/state | None |
| Be placed anywhere in your tree | None |
| Toggle features via props; be themed with CSS | None |
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(propsannotationId,onChangeStatus,disabled) →…Trigger(…TriggerName/…TriggerIcon/…TriggerArrow) +…Content→…ContentItem(propsstatusObj/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.
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:
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.)
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).

