The big one: wireframes don’t run your interactivity (with proof)
Limitation: any ReactonClick, useState, hook, or stateful UI-library component placed inside a wireframe slot does not run. Behavior inside a wireframe comes only from Velt’s Velt…Wireframe.X slot components.
Why: when Velt renders a wireframe, it reduces your slot markup to an HTML string (innerHTML): serializing to a string strips every React event listener, hook, and piece of state. Velt then re-scans the cloned markup and re-instantiates only its own velt-* slot components. Your non-velt-* markup survives as inert DOM; your handlers do not.
Proof (verified live): a probe <button onClick={…}> placed in a wireframe slot rendered as two DOM nodes: the hidden React original (display:none, has React props, its handler fired) and the visible cloned copy injected into the live dialog (no React props, handler did not fire). The visible node: the one users click: has no working handler.
What survives the clone: static elements, CSS className/inline styles, velt-if/velt-class/velt-data tokens, and Velt…Wireframe.X slots. What dies: onClick, useState, hooks, component-library interactivity.
Consequences / what to do:
- Need a working built-in action? Use the slot (
.ResolveButton,.Options.Content.Delete,.Composer.ActionButton). - Need a custom action from wireframe UI? Use
VeltButtonWireframe+useVeltEventCallback("veltButtonClick")(seePatterns and tips). - Need genuinely custom interactive components inside the UI? That’s the signal for headless or primitives.
Shadow DOM
- Limitation: external/global CSS selectors cannot style inside Velt’s shadow DOM. Only CSS variables cross the boundary.
- Fix:
shadowDom={false}on the components you style (R6). - Gotcha: with shadow DOM on, theming via
--velt-*still works, so it looks like only some CSS is ignored: leading to confusion. Decide up front: customizing visuals ⇒ shadow DOM off.
CSS
- Can’t restructure. CSS recolors/respaces; it cannot reorder, add, or remove parts. Reaching for
display:noneto remove a feature is a smell: use a prop or wireframe. - Class names aren’t a stable API. Velt’s internal class names can change between versions; prefer
--velt-*variables. Use class selectors only for gaps, and expect to re-check them on upgrade.
Primitives
- More effort than wireframes. You write the composition Velt would otherwise do: fetch annotations, loop, pass
annotationId/comment, handle conditionals. Layout control is high (compose sub-component primitives), but it’s manual work; if a wireframe slot already expresses the layout, that’s less effort. - Leaf pieces can’t be restructured as primitives. The deepest pieces have no sub-components. To restructure a leaf, use that leaf’s wireframe (works even inside an otherwise-primitive build).
Wireframes
- List/repeater slots ignore custom layout. Some slots (the comments list, presence avatar list, reactions-panel items, activity-log list, …) keep rendering Velt’s own loop: wrapping them in your own grid/flex or reordering does nothing. Customize the repeated item wireframe instead, not the list container. (See list/repeater slots.)
- Default styling isn’t fully stripped. A wireframe replaces a slot’s content, but Velt’s surrounding default styles (borders, padding, backgrounds, fixed widths, popover chrome) remain. Inspect → find the
velt-*class → override with!important. - Scroll/height needs the whole flex chain. Velt’s internal container elements sit between your layout and the scrollable list; if any of them lacks
min-height:0/flex:1/height:100%, the list won’t take available height and scrolling breaks. You must inspect and force the chain through Velt’s internal elements (often with!important).
Positioning
- Absolutely-positioned pieces need a positioned ancestor. Some Velt UI (dialog/pin/overlays, reaction pins) uses
position: absolute. In a primitive or wireframe mount it can land in the wrong place unless the parent you mount it in hasposition: relative. - Can’t change behavior or data shape. Slots give you Velt’s behavior, not custom behavior. If you wish a slot did something different, you’ve outgrown wireframes.
- Variable availability differs per slot.
{comment}/{commentIndex}exist only inside thread-card descendants;{notification}only in the notifications panel;{focusedAnnotation}in the sidebar. A token used in the wrong slot resolves toundefined. (SeeWireframe tokens.) velt-classneeds the class defined in your CSS. The token toggles a class name; you still have to style that class.- One registry only. Two
<VeltWireframe>roots conflict (R1). - Limited/no slots for some features. Arrows and Tags have limited or no wireframe slots: customize them with CSS + props instead.
Headless
- You own everything. Thread layout, replies, composer (mentions/attachments/reactions), filtering, empty/loading states, a11y, dark mode: all yours to build and maintain.
- Upgrade burden. New Velt UI features won’t appear automatically; you re-implement them.
- Object completeness. Hand-built
Comment/CommentAnnotationobjects must include every field the mutation needs (R13): missing fields is the #1 headless bug.
SSR / hydration (Next.js and friends)
- Velt and its custom elements are client-side. Mark customization components and any hook-using component
'use client'. - Gate custom UI on
useVeltInitState()so you don’t render against an uninitialized client. - Don’t rely on Velt UI during SSR: it materializes after hydration + init.
Real-time / multi-user
useCommentAnnotations()(and friends) are reactive: they update when other users change data. Don’t cache/snapshot the array and render stale; render the hook’s current value.

