Skip to main content
What each layer can’t do, the gotchas that waste hours, and the proof behind the wireframe interactivity rule.

The big one: wireframes don’t run your interactivity (with proof)

Limitation: any React onClick, 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") (see Patterns and tips).
  • Need genuinely custom interactive components inside the UI? That’s the signal for headless or primitives.
This is also exactly why the two differ on UI libraries: primitives preserve a UI-library component’s live behavior (state, handlers); wireframes keep only its static components and classes (the cloned copy renders the markup + CSS but loses behavior). Library components for looks can sit inside a wireframe; library components that need to do something must be used with primitives or headless.

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:none to 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 has position: 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 to undefined. (See Wireframe tokens.)
  • velt-class needs 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/CommentAnnotation objects 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.

Quick “why isn’t it working?” table

SymptomLikely causeFix
My CSS does nothingShadow DOM onshadowDom={false} (R6)
Button in my wireframe doesn’t clickInteractivity stripped by cloningUse a Velt slot or veltButtonClick (R4)
velt-data shows blankWrong/undefined variable or wrong slotCheck name + slot context (tokens)
Wireframe renders nothingFeature component not mountedMount VeltComments/sidebar too (R2)
Sidebar/list won’t scroll / take heightFlex/height chain broken (incl. Velt’s internal elements)Force min-height:0/flex:1/height:100% through the chain, inspect Velt internals (R14)
Default styling still showing after wireframingWireframe replaces content, not surrounding default stylesInspect → velt-* class → override with !important (R9b)
Dialog/pin lands in wrong place (top-left)Absolutely-positioned Velt UI, no positioned ancestorGive the mount parent position: relative
Custom dropdown items don’t set status/priorityUsed non-Velt items / missing annotation contextUse Velt primitive …DropdownContentItem (or useUpdateStatus) with annotationId (primitives)
Dark mode colors wrongHard-coded colorsUse --velt-dark-mode-* under [data-velt-theme="dark"] (R9)
Two wireframes fightingTwo <VeltWireframe> rootsUse exactly one (R1)