Skip to main content
Run this decision tree for every design, feature by feature. There is no single “best” layer, but there is a default order to reach for them. Cheaper layers (CSS, wireframes) are faster and upgrade-safe; richer layers (primitives, headless) give more control but you do more work and maintain more.
Three golden principles
  1. Use the cheapest layer that can express the design. Reach in this order: CSS → Wireframes → Primitives → Headless. CSS is always available and layers on top of every approach.
  2. Wireframes are the default for structural customization. Escalate to primitives only for your own UI library or interactivity; headless is the last resort.
  3. You can mix layers, even on the same surface. One caveat: on anchored surfaces (pin dialog, pins, bubbles, cursors), dropping below wireframes forfeits Velt’s positioning engine; see Behaviors.

How to use this

Split the design into features/surfaces (comment dialog, comments sidebar, comment pin, notifications panel, …) and walk the questions below once per surface; stop at the first “yes”. Different surfaces can land on different layers, and one surface can mix layers; that’s expected. Not sure which Velt component a design element is? Match it first in Component definitions.

The questions (per feature)

Q1: Is the only difference colors, spacing, fonts, radius, or shadows? (The layout and which elements appear are basically Velt’s default.) → CSS. Theme with --velt-* variables; override classes with !important. See CSS. Q2: Does the design change the structure/layout (custom header, reordered parts, custom thread-card, custom empty state) while the features stay the same, with no interactive components of yours inside?Wireframes (+ CSS). This is the default for structural customization: prefer it. Velt fetches the data, loops the threads/comments, and wires each slot’s behavior; you just supply layout markup. See Wireframes. Q3: Do you need full control: your own UI component library, your own interactivity/state, or to compose Velt’s building blocks yourself and place them anywhere in your tree?Primitives (+ CSS, + a leaf’s wireframe where needed). You write the React (loop annotations, pass annotation-id, loop comments, pass each comment); you compose the building-block components and their sub-components into any layout; you wrap them in any UI library. More effort, most control. See Primitives. Q4: Does nothing Velt renders fit: you must own 100% of the UI, or render on a surface Velt can’t draw into (PDF, canvas, video timeline)?Headless (last resort). Data + actions via hooks; you build everything. See Headless. Still unsure between wireframes and primitives? Default to wireframes and only switch to primitives when you hit one of the Q3 triggers (UI library, custom interactivity, leaf-deep changes, arbitrary placement).

Flowchart


After you pick: eight quick checks

Q1-Q4 pick the primary layer. These checks refine it; run through them once per surface.
Many features are off by default (reply avatars, priority, minimap, @here, device indicator, comment index / pin number, sidebar-button-on-dialog, format toolbar, …). If the design shows one, enable it with its prop (e.g. priority, replyAvatars, commentIndex, sidebarButtonOnCommentDialog): look up the exact prop in Feature flags. Enabling a documented feature is not a hack and is not a reason to escalate layers.
Custom statuses / priorities / categories / reactions → configure customStatus / customPriority / customCategory / customReactions (Component config). The design surfaces your app’s own data in the comment UI → that’s context. Neither changes the layer: they’re data config layered on top.
If the design is built from your own component library, where those components sit decides wireframe vs primitive. Static shell + classes only → wireframe is OK; must stay interactive → primitive. See the next section.
A surface can combine layers: wireframe most of it + use a leaf’s wireframe for one piece; or a primitive for the surface + a leaf wireframe for one part; + CSS on either. Note the per-piece split, and keep one <VeltWireframe> and one stylesheet globally (Combining approaches).
If the design requires your logic to decide when a piece appears (not Velt’s internal condition), that’s a primitive with defaultCondition={false}: wireframes have no equivalent (Primitives). This can flip a Q2 “wireframe” answer to a primitive; resolve it here.
  • Wireframing a surface’s ROOT wireframe (e.g. VeltCommentDialogWireframe, VeltCommentPinWireframe) → Velt auto-removes that surface’s shadow DOM → your class CSS reaches it, no flag needed.
  • Wireframing only a nested / leaf slot (no root wireframe for that surface) → shadow is not removed → set shadowDom={false} (or the per-surface flag / injectCustomCss) for class CSS.
  • CSS variables (--velt-*) and inline style="" always work, shadow or not. See CSS. (Which wireframes are roots: the per-surface map in Component catalog.)
If the surface is anchored (pin dialog, pins, bubbles, cursors, minimap), Velt’s host owns its on-page position: the dialog component itself only requests re-anchoring and has no position input. Wireframes keep that positioning for free; primitives/headless mean you own it, which for a pin-anchored dialog may not even be feasible via the public API. Treat this as a strong reason to stay at wireframes; escalate only after a positioning path is verified feasible. Statically-placed surfaces have no penalty. See Behaviors.
Before concluding a goal needs primitives/headless or is not possible, confirm in Data models that the required fields/events exist. If they do → feasible via headless at worst. If they’re a documented absence → genuinely not achievable client-side. Name the first layer that achieves the goal (default behavior → prop/config → wireframe → primitive → headless) and go with that. If these docs don’t cover it, check against the live SDK in a running app before declaring it impossible.
By the end you should know, per surface: the layer (or mix), which feature flags to enable, any custom-data config, where your UI-library components sit, shadow-DOM handling, and whether the surface is anchored. With those answered, the build is mechanical.

The UI-component-library question (don’t miss this)

If your design is built from your own component library (MUI, shadcn/ui, Ant, Radix, Chakra, Tailwind UI…), where those components live changes everything: Rule of thumb: want to reuse your interactive design-system components inside the collaboration UI?Primitives (or Headless). Not wireframes.

Comparison matrix


When a layer “breaks down” → escalate

Signs you’ve outgrown your current layer:
  • CSS → Wireframes: you’re writing display:none to hide parts, or you need to reorder/add/remove parts or restructure the header/composer/thread-card layout.
  • Wireframes → Primitives: you need your own interactive component inside the UI, your own UI component library there, to place Velt pieces arbitrarily in your tree, custom data composition/conditionals beyond velt-if, or to customize a leaf piece more than its slot allows. (For a single leaf you can stay in wireframes and use that leaf’s wireframe; switch to primitives when the whole surface needs that level of control.)
  • Primitives → Headless: Velt can’t render where you need it (PDF/canvas), or you want to own 100% of the data→view pipeline.
And the reverse: don’t over-escalate:
  • Went primitives just to restructure layout that wireframe slots already expose? Wireframes are less work.
  • Went headless just to recolor or reflow? CSS or wireframes would do it.

Worked examples

Picked your layer or layer mix? Head to Setup, then open the matching approach page.
Have this design in Figma? The UI Customization Plugin for Cursor and Claude Code turns a Figma design into verified Velt UI customization (comments and notifications today): it runs this decision tree on your design surface-by-surface, then builds and verifies each result.