Skip to main content
What it is: keep all of Velt’s behavior and data wiring, but supply your own HTML layout for each piece of the UI. You decompose a Velt component into named slots (header, thread card, composer, …) and fill each slot with your markup. Velt does the heavy lifting: it fetches the data, loops the threads/comments, and wires each slot’s behavior; you only lay out the slots. This is the default for structural customization: prefer it. It’s less work than primitives precisely because Velt handles the data/looping/wiring for you (with primitives you’d write all of that yourself). Use it when: the design changes the structure/layout of Velt’s UI (custom header, reordered parts, custom thread-card, custom empty state) while the features stay the same, and the custom parts are non-interactive markup. (Decision tree Q2.) Don’t use it when: you need your own interactive components / UI-library widgets inside the UI (→ primitives), or you only need recoloring (→ css).
This page is the concepts and rules. The rest of the section:
Read the interactivity rule before you write any wireframe. It is the #1 source of wireframe bugs.

The model

Two pieces work together:
  1. <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).
  2. Velt…Wireframe slot components: e.g. VeltCommentDialogWireframe, VeltCommentsSidebarWireframe, with nested static slots like .Header, .Body, .ThreadCard, .Composer. You fill these with your own markup.
Then, separately, you mount the normal feature component (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.

Quick example

Register a template inside the (single) <VeltWireframe> registry, then mount the live feature component as normal. Your own elements provide structure and visuals; the Velt…Wireframe.X slots are where Velt’s behavior renders.
Full step-by-step setup, with Other Frameworks equivalents for every step: Setup Wireframes.
Slots take inputs too. Some slots accept props, such as Composer.ActionButton type="submit", Composer.Input placeholder="...", and ThreadCard.Reactions excludeReactionIds={[...]}. The complete slot list, per-slot props, and every wireframe component are in Wireframe components.

What it can and can’t do


Notes & deep-dives

1. 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 ThreadCard layout placed inside VeltCommentDialogWireframe customizes 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, …).
Mechanism: the registry is a flat global map keyed by component name (+ optional 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.

2. 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 a variant name. This is how you give one component different looks in different contexts (floating dialog vs sidebar row vs focused thread vs page-mode composer). The mechanics, briefly:
  • Register: variant="<name>" on the Velt…Wireframe (internally keyed as component---<name>).
  • Select: the matching …Variant prop on the live component (variant, dialogVariant, focusedThreadDialogVariant, pageModeComposerVariant).
  • Fallback: no matching variant means the base (no-variant) wireframe renders.
Step-by-step create/use walkthroughs, including pre-defined variants: Layout Customization → Variants.

3. 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).
Container slots are the exception: verified in-browser. The fallback rule holds for leaf slots, but the moment you declare a structural/container slot (a feature root like velt-comments-sidebar-v2-wireframe, or a parent like the sidebar panel/header), you own its layout: any structural children you don’t declare inside it disappear: they do NOT fall back to Velt’s default. Tested live: a sidebar root wireframe containing only a custom empty-placeholder rendered the empty state correctly but dropped the search box, filter buttons, and list (which the default empty state shows). Fix: declare the full structural tree you want inside the container (e.g. panel → header(search, filter) → list → empty-placeholder), exactly as the SDK’s own wireframe examples do. Rule of thumb: override a leaf and the rest stays; override a container and you must re-declare its children.
The complete slot list per feature: Wireframe components. Worked targeted-vs-full-tree examples: Layout Customization.

4. 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 to undefined. Never invent one. Syntax: Wireframe tokens; full catalog: Wireframe variables.

5. 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:
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:
If your design genuinely needs custom interactive behavior that no slot provides, that’s the signal to go headless.

6. 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.

7. 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.
Rule of thumb: if a slot represents a list of things, don’t relayout the list: restructure the item. (If you truly need a custom list layout/virtualization, that’s a signal for primitives, where you own the loop.)

8. How to discover slots and variables

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 from Velt…Wireframe.X slots 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.