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. You can see this yourself in DevTools: a <button onClick={…}> placed in a wireframe slot renders as two DOM nodes: the hidden React original (display:none, has React props, its handler works) and the visible cloned copy injected into the live dialog (no React props, handler does 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.
  • 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 Conditional Templates.)
  • 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.
  • 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; 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