The model
Two pieces work together:<VeltWireframe>: an invisible registry (display:none). You put your wireframe templates inside it. Use one per app (extra roots merge first-with-content-wins → conflict-prone; see R1).Velt…Wireframeslot components: e.g.VeltCommentDialogWireframe,VeltCommentsSidebarWireframe, with nested static slots like.Header,.Body,.ThreadCard,.Composer. You fill these with your own markup.
VeltComments, VeltCommentsSidebar, VeltCommentDialog, …). It renders using the template you registered.
So a wireframe doesn’t render anything by itself: it registers a template that the live feature component picks up.
Steps
Register one VeltWireframe root
Put every feature wireframe inside a single registry, rendered once near your app root (this is
VeltCustomization.tsx in the reference structure):Fill a feature's slots with your layout
Inside a Then mount the live feature somewhere (it renders using the template above):
Velt…Wireframe, lay out the slots however your design wants. Your own elements (<header>, <div className="…">) provide structure/visuals; the Velt…Wireframe.X slots are where Velt’s behavior renders.What it can and can’t do
| ✅ Wireframes can | ❌ Wireframes can’t |
|---|---|
| Give Velt’s UI any layout/structure you want | Run your React handlers/state/hooks inside slots |
| Add non-interactive custom markup around Velt’s parts | Host live UI-library components (interactivity is stripped) |
Conditionally render via velt-if and show data via velt-data | Change Velt’s behavior (slots give you Velt’s behavior, not custom) |
| Override only the slots you care about | Relayout a list/repeater slot: customize its item instead (list/repeater slots) |
| Customize each row via its item slot | Be split across multiple <VeltWireframe> roots |
Notes & deep-dives
3b. Scoping: global vs scoped wireframes (important)
Where you place a child wireframe changes where it applies:- Child wireframe nested inside its parent wireframe → scoped to that parent’s render. It travels as part of the parent’s cloned subtree, so it only customizes the child as it appears inside that parent. Example: a
ThreadCardlayout placed insideVeltCommentDialogWireframecustomizes thread cards in the dialog, not elsewhere. - Child wireframe placed directly at the
<VeltWireframe>root → global. It registers under its own key and applies to that component everywhere it renders (dialog, sidebar, inline section, …).
variant/suffix), never by parent. The root <VeltWireframe> scan registers only its direct children as global keys; a nested child isn’t a direct child, so it isn’t registered globally: it rides inside the parent’s clone. Collisions resolve first-with-content-wins, so a global/root definition is not overwritten by a nested one. Practical rule: nest to scope, root-level to go global; don’t register the same component both ways.
3c. Variants: multiple looks for the same component
By default a component has one registered wireframe. Variants let you register several wireframe templates for the same component and choose which one renders, by avariant name. This is how you give one component different looks in different contexts: without that, a component looks identical everywhere it appears.
- Register a variant: put
variant="<name>"on theVelt…Wireframe. Its template is registered under that variant (internally keyed ascomponent---<name>). - Select a variant: the live component renders the wireframe whose variant matches its current variant. You set that with the component’s variant prop(s): the general
variant, and because the comment dialog is reused in several contexts:dialogVariant,focusedThreadDialogVariant, andpageModeComposerVariantpick the variant for each of those contexts. - Fallback: if no wireframe matches the active variant, Velt falls back to the base (no-variant) wireframe for that component. So “one base wireframe + a couple of variant wireframes” is the normal setup.
variant on the wireframe and the …Variant prop on the component.)
4. Slot granularity: override only what you want
The slot tree is very fine-grained (hundreds of slots across the SDK). For example, inside a comment dialog you’ll find slots such as: composer →Input / Attachments (image and other, each with delete/download/loading sub-slots) / AssignUser; thread-card → Avatar / Attachments / Name / Time / Message / Options / Reactions; assignee-banner → ResolveButton / UnresolveButton; options dropdown → Edit / Delete.
You only fill the slots you care about: a slot you never declare falls back to Velt’s default. This means you can do a tiny override (just the empty state) or a near-total rebuild (overriding 40+ slots across the dialog/sidebar). Two exceptions to the fallback rule: container/structural slots (see the warning below: declare a container and you own its whole child tree) and list/repeater slots (see list/repeater slots: Velt keeps rendering its own loop; customize the item, not the wrapper).
The complete slot list per feature: Wireframe components.
5. Tokens: conditional rendering, classes, and live data
Inside wireframe markup you can read live state with{…} tokens and use them three ways. (Full syntax + the 240+ variable names: Wireframe tokens.)
velt-if: show/hide based on a condition:
velt-if supports logical/comparison operators, e.g. condition="{enableResolve} && {canResolveAnnotation}" or condition="{commentIndex} === 0".
velt-data: print a live value as text:
velt-class: toggle CSS classes conditionally:
Where do variable names come from? They’re a fixed set ({user},{annotation},{comment},{commentIndex},{noCommentsFound},{darkMode}, …). A name not in the catalog resolves toundefined. Never invent one. Syntax:Wireframe tokens; full catalog:Wireframe variables.
6. The interactivity rule (the single most important thing)
Inside a wireframe, your own React interactivity does NOT run. Behavior comes only from Velt’s Velt…Wireframe.X slot components.
When Velt renders a wireframe, it copies your slot markup into its own render tree (technically: it serializes your slot to HTML and re-instantiates only the velt-* slot elements inside it). The copy is plain DOM. That means, for markup you put in a slot:
| In your wireframe markup | Survives into the live UI? |
|---|---|
Static elements (<div>, <span>, <header>, icons) | ✅ yes |
Your UI-library components used as static presentation (a <Card>, <Badge>, styled button shell) | ✅ yes: their rendered markup + CSS classes survive |
CSS className / inline styles | ✅ yes |
{…} tokens (velt-if, velt-class, velt-data) | ✅ yes (Velt resolves them) |
Velt…Wireframe.X slot components | ✅ yes: this is where behavior comes from |
Your React onClick, useState, hooks | ❌ no: silently dead |
| A UI-library component’s behavior (its own click/state/effects) | ❌ no: only its static markup renders |
Using your UI library in wireframes = static components and classes. You can drop in your design-system components for their look (markup + classes survive the clone), but their interactivity does not run. For interactive library components, use primitives.Why: Velt reduces wireframe slot content to an HTML string, which strips React listeners; it then re-instantiates only its own
velt-* components. A probe <button onClick> placed in a wireframe slot does not fire on the rendered (cloned) copy. More detail: Edge cases and limitations.
What to do instead: want a working button? Use the Velt slot for it (e.g. VeltCommentDialogWireframe.ResolveButton, .Options.Content.Delete, .Composer.ActionButton). Your markup goes inside that slot as its appearance:
7. Page-mode (a common wireframe scenario)
“Page mode” renders the comments sidebar anchored to elements on your page (e.g. one thread per form question), with a per-element comment-count bubble and a page-mode composer. It’s still just wireframes: you fill the sidebar/thread/composer slots and the comment-bubble count slot to get custom comment cards, attachments, reactions, and assignment rows, all while Velt keeps the behavior.
Page mode usually goes hand-in-hand with context: attaching your domain data (e.g. the question id/title) to each comment and reading it back in the dialog/composer. See Context.
7b. List/repeater slots: custom layout is ignored (the replaceTemplate: false case)
A few slots are list/repeater containers: they render a loop of child items (e.g. the comments list, the presence avatar list, the reactions panel items, the activity-log list). These slots run in a mode where your custom layout/markup around them does NOT replace the container: Velt keeps rendering its own loop and ignores wrapper divs or reordering you put there.
What this means in practice:
- ❌ Wrapping a list slot in your own grid/flex layout, or adding sibling markup inside it, won’t take effect: the default loop renders regardless.
- ✅ Customize the repeated item instead. Velt passes a child/item template directly to the specific child component, so you style/restructure each row via its own item wireframe (e.g. customize the list item, not the list). The list keeps Velt’s looping; you control how one item looks.
8. How to discover slots and variables
- Slots & slot props:
Wireframe components(every wireframe + full slot trees) and the overview inComponent catalog. - Variables/tokens: syntax in
Wireframe tokens; the full{…}catalog inWireframe variables. - Stateful CSS classes (to style state without a slot):
CSS classes.
Checklist
- Exactly one
<VeltWireframe>in the app. - The live feature component (
VeltComments/VeltCommentsSidebar/VeltCommentDialog) is mounted in addition to the wireframe. - No React
onClick/useState/hooks inside slot markup: interactivity comes fromVelt…Wireframe.Xslots only. - For list/repeater slots, customized the item, not the container layout (list/repeater slots).
- Only real slot names (
Wireframe components) and real{…}variables (Wireframe variables). -
shadowDom={false}if you style the result. - Decided scope per child wireframe: nested = scoped, root = global (scoping).
- Unfilled slots intentionally left to Velt defaults.

