Skip to main content
Keep all of Velt’s behavior and data wiring, but supply your own HTML layout. You decompose a Velt component into named slots (header, thread card, composer, …) and fill each slot with your markup. Velt fetches the data, loops the threads and comments, and wires each slot’s behavior; you only lay out the slots. This is the default for structural customization, and it’s less work than primitives, where you’d write the data plumbing yourself. Use it when the design changes the structure of Velt’s UI (custom header, reordered parts, custom thread card, custom empty state) while the features stay the same, and your custom parts are non-interactive markup. Don’t when you need your own interactive components inside the UI (that’s primitives) or you only need recoloring (that’s CSS). Still unsure? Run the decision tree.
This page is the concepts and rules. The rest of the section:Prefer automation? The UI Customization Plugin builds wireframe customizations from a Figma design and verifies them in a real browser.

The model

Two pieces work together:
  1. <VeltWireframe>: an invisible registry (display:none). You put your wireframe templates inside it. Use one per app: more than one merges first-with-content-wins, which causes hard-to-debug conflicts.
  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.

The interactivity rule

Read this before you write any wireframe. It is the #1 source of wireframe bugs.
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: it serializes your slot to HTML and re-instantiates only the velt-* slot elements inside it. The copy is plain DOM, which strips React listeners. So for markup you put in a slot: What to do instead: want a working button? Use the Velt slot for it (VeltCommentDialogWireframe.ResolveButton, .Options.Content.Delete, .Composer.ActionButton, …). Your markup goes inside that slot as its appearance:
You can drop in your design-system components for their look, since markup and classes survive the clone, but their interactivity won’t run. For interactive library components use primitives; for behavior no slot provides, go headless.
See it for yourself. Put a <button onClick={…}> in a wireframe slot and open DevTools: it renders as two nodes. The hidden React original (display:none) still has its React props and a working handler. The visible cloned copy injected into the live dialog has no React props, and its handler never fires. The node users can actually click is the one without the handler.

Scoping: global vs scoped wireframes

Where you place a child wireframe changes where it applies:
  • 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. A ThreadCard layout placed inside VeltCommentDialogWireframe customizes thread cards in the dialog, not elsewhere.
  • 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, …).
Why: the registry is a flat map keyed by component name (plus optional variant), never by parent. The root scan registers only its direct children as global keys, so a nested child isn’t registered globally: it rides inside the parent’s clone. Collisions resolve first-with-content-wins, so a root definition is not overwritten by a nested one. Rule: nest to scope, root-level to go global; don’t register the same component both ways.

Slot granularity

The slot tree is very fine-grained (hundreds of slots across the SDK). Inside a comment dialog alone you’ll find composer → Input / Attachments / AssignUser; thread-card → Avatar / Name / Time / Message / Options / Reactions; options dropdown → Edit / Delete. You only fill the slots you care about: a slot you never declare falls back to Velt’s default. So you can do a tiny override (just the empty state) or a near-total rebuild (40+ slots across the dialog and sidebar).
Container slots are the exception. The fallback rule holds for leaf slots. Declare a structural/container slot (a feature root like velt-comments-sidebar-v2-wireframe, or a parent like the sidebar panel/header) and you own its whole child tree: structural children you don’t declare disappear rather than falling back. A sidebar root wireframe declaring only a custom empty-placeholder renders the empty state but drops the search box, filter buttons, and list. Fix: declare the full tree you want inside the container (panel → header(search, filter) → list → empty-placeholder). Override a leaf and the rest stays; override a container and you re-declare its children.
List and repeater slots are the other exception. The complete slot list per feature: Wireframe components. Worked targeted-vs-full-tree examples: Layout Customization.

Variants

By default a component has one registered wireframe. Variants let you register several templates for the same component and choose which one renders by name, so one component can look different in different contexts (floating dialog vs sidebar row vs focused thread vs page-mode composer).
  • Register: variant="<name>" on the Velt…Wireframe.
  • Select: the matching prop on the live component (variant, dialogVariant, focusedThreadDialogVariant, pageModeComposerVariant). Which ones a given component accepts is listed per component in Props.
  • Fallback: no matching variant means the base (no-variant) wireframe renders.
Create/use walkthroughs, including pre-defined variants: Layout Customization → Variants.

List and repeater slots

A few slots are list/repeater containers: the comments list, the presence avatar list, the reactions panel items, the activity-log list. These keep rendering Velt’s own loop, so your layout around them is ignored.
  • ❌ Wrapping a list slot in your own grid/flex layout, or adding sibling markup inside it, won’t take effect.
  • Customize the repeated item instead. Velt passes the item template straight to the child component, so you restructure each row through its own item wireframe.
Rule: if a slot represents a list of things, don’t relayout the list, restructure the item. If you truly need a custom list layout or virtualization, that’s a signal for primitives, where you own the loop.

Tokens: live data in your markup

Wireframe markup reads Velt’s live state through {…} tokens. There are three things you can do with one: Variable names are a fixed set ({user}, {annotation}, {comment}, {commentIndex}, {noCommentsFound}, {darkMode}, …). A name outside the catalog resolves to undefined, so never invent one: the full list is in Template Variables.

Page mode

“Page mode” renders the comments sidebar anchored to elements on your page (one thread per form question, for example), with a per-element comment-count bubble and a page-mode composer. It’s still just wireframes: fill the sidebar, thread, composer, and bubble-count slots and Velt keeps the behavior. Page mode usually pairs with context: attaching your domain data (the question id or title) to each comment and reading it back in the dialog or composer. See Context.

What it can and can’t do

Where to look things up

Troubleshooting

Common wireframe symptoms, in Debugging:

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 and repeater slots).
  • Only real slot names (Wireframe components) and real {…} variables (Template 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.