Skip to main content
What it is: the four layers are not an app-wide setting: you pick CSS / wireframes / primitives / headless per feature/surface and combine them freely, then layer CSS on top of everything for theming. Use it when: any real app where different features need different layers: the normal case. (Run the decision tree on each feature.) Don’t use it when: a single surface genuinely needs only one layer, then just follow that layer’s page directly (css · wireframes · primitives · headless).
Use one <VeltWireframe> registry in the whole app: everything else mixes without restriction.

The model

The four layers are not an app-wide setting. You choose per feature/surface, and you can combine them freely. Most real apps mix two or more. The core idea: pick the cheapest layer per feature, then layer CSS on top of everything for theming.

The one rule for mixing

Use one <VeltWireframe> registry in the whole app: it may contain many feature wireframes. (Multiple are technically possible but merge first-with-content-wins: order-dependent and conflict-prone, so keep it to one; see R1.) Everything else mixes without restriction.
CSS variables apply globally, so they compose with any layer. Primitives, wireframes, and headless components can all coexist in the same app, even for related features. Same-surface mixing is allowed and often useful. You can drop in a VeltCommentDialog primitive and use a wireframe to customize parts of that same dialog: for example, primitive everything, but wireframe a single leaf piece (leaf primitives have no sub-components, so their wireframe is the only way to restructure them). What matters is choosing intentionally per piece; there’s no rule against using more than one layer on a surface. (CSS is always layered on top regardless.)

Per-feature selection (how to think)

List your features, run the decision tree on each, record the result:
Feature/surfaceLikely layerNote
Comment dialog / threadCSS · Primitive · Wireframe · HeadlessMost commonly wireframed when the design is custom.
Comments sidebarCSS · Primitive · WireframeWireframe for custom panel layout; primitive if default is fine.
Comment pin / bubbleCSS · WireframeSmall, often just CSS or a tiny bubble wireframe.
Notifications panelCSS · Primitive · WireframeSame logic.
Reaction toolCSS · PrimitiveUsually CSS.
A count badge in your headerHeadlessData-only via a hook, no Velt UI.
Different rows landing on different layers is normal and correct.

Common combinations

CSS + Primitives: “use it as-is, on-brand, in my design system”

Drop in Velt components, toggle features with props, wrap in your UI library, theme with --velt-*.

CSS + Wireframes: “custom layout, branded”

Override the slots you need; theme the result with one stylesheet. The most common combination in practice.

Wireframes + Primitives: “custom dialog, default sidebar” (different surfaces, or same surface)

Wireframe one surface, use another as a plain primitive. Or, on one surface, use a primitive and wireframe a leaf piece of it. They don’t interfere.

Wireframes + Headless: “custom UI + data-driven extras”

Wireframe the visible comment UI, and use a read hook to render something in your chrome (unread badge, counts, a custom filter control).

Headless-only: “Velt is just the data layer”

No Velt UI at all; you render everything from hooks.

A concrete mixed setup

// 1) Global theme (CSS): applies to everything
import "./velt/ui-customization/styles.css";

// 2) One wireframe registry (custom dialog layout)
<VeltCustomization />            {/* the single <VeltWireframe> */}

// 3) Live feature components
<VeltComments shadowDom={false} />               {/* renders dialogs via the wireframe */}
<VeltCommentsSidebar shadowDom={false} />        {/* default sidebar = primitive */}

// 4) A headless extra in your own header
function HeaderBadge() {
  // returns an object ({ count } | null): read .count, don't render the object
  const unread = useUnreadCommentAnnotationCountOnCurrentDocument();
  return <span className="badge">{unread?.count ?? 0}</span>;
}
Here: dialog = wireframe, sidebar = primitive, badge = headless, all themed with CSS, one <VeltWireframe>. This is a typical, healthy mix.

Anti-patterns

  • Two <VeltWireframe> roots. Merge first-with-content-wins → conflict-prone. Use one.
  • Headless for a feature a wireframe could do. Unnecessary maintenance: see Headless.
  • Primitives just to restructure layout that wireframe slots already expose. More work for the same result: wireframe it.
  • CSS display:none to remove a feature when a prop or wireframe would do it cleanly.
Note: mixing layers on the same surface is not an anti-pattern (e.g. primitive dialog + wireframe leaf). The anti-pattern is choosing a more expensive layer than the design needs.

What it can and can’t do

✅ Combining can❌ Combining can’t
Mix CSS, wireframes, primitives, and headless per feature/surface in one appUse more than one <VeltWireframe> registry cleanly: keep it to exactly one per app
Layer CSS theming on top of any other layer (variables apply globally)Justify a layer more expensive than the design needs (the core anti-pattern)
Use multiple layers on the same surface (e.g. primitive dialog + wireframe leaf)Escape each layer’s own rules: wireframe interactivity limits and headless cost still apply
Pick the cheapest viable layer per piece via the decision tree

Checklist

  • One <VeltWireframe> total.
  • Cheapest viable layer chosen per feature/piece: CSS → Wireframes → Primitives → Headless (ran the decision tree on each).
  • Each piece’s layer chosen intentionally (same-surface mixing is fine).
  • One shared stylesheet for all --velt-* theming + !important overrides.