Headless is the most powerful and most expensive layer: reach for it only when no other layer fits.
The model
Three kinds of hooks (full list grouped inHooks):
- Read (data/state):
useCommentAnnotations,useCommentAnnotationById,useUnreadCommentAnnotationCountOnCurrentDocument,useCommentModeState, … → reactive data you render. - Mutate (actions):
useAddCommentAnnotation,useAddComment,useUpdateComment,useDeleteComment,useDeleteCommentAnnotation,useResolveCommentAnnotation,useUpdateStatus,useToggleReaction,useGetLink, … → call these from your own buttons. - Control:
useVeltClient,useSetDocuments,useIdentify,useVeltInitState, … → init/scope/imperative control.
Steps
Step 1: Render data from a read hook
useCommentAnnotations() is reactive: it re-renders when comments change anywhere (including other users in real time). You never poll.
Step 2: Wire your buttons to action hooks
Your components are fully interactive (unlike wireframes). Call the action hooks from your own handlers:Step 3: Build objects to the SDK data model
Mutations expect objects shaped like Velt’s types (from@veltdev/types). You construct them yourself. Example Comment builder:
Because you hand-build these, read the type (MinimalComment,CommentAnnotation,Userin@veltdev/types) and fill every field the action needs. Missing fields are the most common headless bug.
Comment for addComment / updateComment (from the Comment type): commentId (number: auto if omitted), type ('text' | 'voice', default 'text'), from (a full User: required), commentText, commentHtml, status ('added' | 'updated'), and the array fields you touch (attachments, taggedUserContacts, reactionAnnotationIds). from is the field most often missed.
What each action expects (the request object)
Action hooks take a single request object, not loose args. The required fields per common mutation (optional ones omitted; all also acceptoptions?):
| Hook | Request fields (required) | Notes |
|---|---|---|
useAddCommentAnnotation → addCommentAnnotation | { annotation: CommentAnnotation } | Creates a new thread; build the full CommentAnnotation. |
useAddComment → addComment | { annotationId: string; comment: Comment } | Adds a reply to an existing thread. Optional: assignedTo?: User, assigned?: boolean. |
useUpdateComment → updateComment | { annotationId: string; comment: Comment } | Optional merge?: boolean to patch rather than replace. |
useDeleteComment → deleteComment | { annotationId: string; commentId: number } | commentId is a number. |
useResolveCommentAnnotation → resolveCommentAnnotation | { annotationId: string } | Toggles resolved. |
useUpdateStatus → updateStatus | { annotationId: string; status: CustomStatus } | status is a full CustomStatus object ({ id, name, color, type, … }), not a string: see Component config. |
useToggleReaction → toggleReaction | { annotationId: string; commentId: number; reaction: { reactionId: string } } | Optional reaction.customReaction?. |
Exact, current shapes are the*Requestinterfaces in@veltdev/types(AddCommentRequest,UpdateStatusRequest, …): read the type if a call errors. The table is the field-level companion to R13 (“fill every field the action needs”).
What it can and can’t do
| ✅ Headless can | ❌ Headless can’t |
|---|---|
| Give you 100% custom UI: Velt renders nothing; you build every component | Save you from re-implementing and maintaining thread layout, the composer (@mentions, attachments, reactions, formatting), filtering/sorting, empty/loading states, accessibility, and dark mode |
| Expose all data + actions through React hooks (Velt still does storage, sync, mentions, permissions) | Auto-inherit new Velt UI features: the upgrade burden is yours; new features won’t appear in your UI automatically |
| Render comments onto a non-DOM surface (PDF, canvas, a video timeline) where Velt can’t draw | Be the cheap choice: it’s the most expensive layer; if a wireframe can express the layout, use it instead |
| Build a panel entirely from your own interactive design-system components, and drive bespoke workflows/filtering via hooks + the client API |
Notes & deep-dives
The cost (be honest about this)
Going headless means you re-implement and maintain everything Velt’s UI gave you for free:- Thread layout, replies, collapsing, edit-in-place, resolved state.
- The composer: @mentions UI, attachments, reactions, formatting.
- Filtering/sorting (compute client-side over
useCommentAnnotations()). - Empty/loading states, accessibility, dark mode.
- Upgrade burden: new Velt features won’t appear in your UI automatically.
When headless is the right call
- Comments rendered onto a non-DOM surface (PDF, canvas, a video timeline) where Velt can’t draw.
- A panel built entirely from your own interactive design-system components.
- Behavior that no slot offers (custom inline workflows, bespoke filtering UIs): drive Velt via hooks + the client API.
Checklist
- Using real hook names from
Hooks. - Objects passed to mutations match
@veltdev/types(all required fields). - Reactive data comes from read hooks (no manual polling).
- You’ve confirmed a wireframe genuinely can’t do it (headless is the costly last resort).

