Skip to main content
Reusable recipes, grouped by the approach they belong to: jump to the bucket for the layer you’re using: All class-based CSS uses !important (R9b: Velt’s runtime styles are high-specificity).

General workflow (any approach)

Progressive escalation. Start at the cheapest layer; escalate only when blocked: CSS → Wireframes → Primitives → Headless. Never start at headless “to be safe”: it’s the most expensive. See Decision tree. Per-feature layering. Decide approach per surface; theme everything with one CSS file. Typical healthy mix: wireframe the dialog, primitive sidebar, headless badge in your header, CSS on top. See Combining approaches. Gate rendering on init. Velt is client-side: don’t render custom UI against an uninitialized client:
Compare against default. When something looks wrong, temporarily remove your customization to see Velt’s default: it isolates “my styling” from “Velt behavior.” One feature at a time. Finish + verify a surface before starting the next. Look up every name. CSS var / class / slot / {…} / prop / hook → it’s in reference/ or it doesn’t exist. The reference map: Wireframe components (slots), Wireframe variables (tokens), CSS classes (stateful classes), Component config (props). 'use client' (Next.js): customization components and anything using hooks must be client components.

CSS-specific

Theme-token layer. Define your brand once as your own variables, then map them onto --velt-*:
One place to change your brand; Velt + your app stay in sync. Reveal actions only on hover. Hide the kebab/options/reaction icons; show on hover. Velt already wires this on .velt-thread-card--container:hover; you can also force it with .velt-thread-card--show-actions.
Reaction tool when empty vs reaction panel when populated. The pin carries velt-reaction-pin--no-reactions only when count is 0. Use it (and :has(app-reaction-pin)) to swap/position the reaction UI by count.
Style the unread dot.
Style resolved threads (no --resolved class: detect the unresolve button):
Hide a container when its data slot is empty.
Resize the default avatar.
Neutralize an SDK popover so your menu takes over.
Indent threaded replies.

Wireframe-specific

Reuse one wireframe template across surfaces. Define a wireframe template once (e.g. your own DialogWireframe component) and register it inside your single <VeltWireframe>; every place that renders that component picks it up. One source of truth for a surface’s look. Conditional UI with velt-if. Branch on live state: empty states, role-gated bits, root-vs-reply:
Show live data with velt-data. Render context/metadata Velt owns:
Conditional empty state. Different copy for “no comments yet” vs “filtered to zero”:
Count badge only when there are comments.
Custom skeleton/loading. Fill the .Skeleton slot with your own loading cards so the loading state matches your design.

Bridge a wireframe button to your app logic (the onClick escape hatch)

You can’t use onClick inside a wireframe (R4). For a custom action, use VeltButtonWireframe + the veltButtonClick event:
For Velt’s built-in actions (resolve, delete, submit) use the dedicated slot, not this. VeltButtonWireframe props: Every click emits veltButtonClick with buttonContext (clicked id, group, selection state) plus the surrounding commentAnnotation / comment when rendered inside a comment surface.

Reset a VeltButtonWireframe after app logic runs

Use resetVeltButtonState() when your app handles a veltButtonClick and needs to return the button to its idle state. This is most useful for custom action buttons that show a selected, loading, or submitted state while your app logic runs.
See resetVeltButtonState() for the API shape. Custom accept/reject (or any custom action) buttons in a thread card. Same mechanism:
Customize a list row, not the list. List/repeater slots ignore custom layout: restructure the repeated item wireframe instead. See list/repeater slots. Scope deliberately. Nest a child wireframe inside its parent to scope it to that parent’s render; place it at the <VeltWireframe> root to apply globally. See wireframe scoping.

Primitives-specific

Trim, don’t hide. Turn features off with props (reactions={false}, attachments={false}) rather than display:none. Cleaner DOM, no dead behavior. Compose sub-components for custom layout. There’s a sub-component primitive for nearly every child: rebuild a header/thread-card by arranging its sub-components, instead of accepting the default layout. (Restructure a leaf via that leaf’s wireframe.) Wrap in your UI library. Primitives are real React components, so your library’s components (and their interactivity) work normally around them: <MuiCard><VeltCommentDialog/></MuiCard>.

Mixed (more than one layer together)

Custom dropdown: your UI library + Velt primitive items + wireframe-styled items. Build the header dropdown (status / priority / options) from your UI-library shell, Velt primitive dropdown items (which carry the set behavior), and a wireframe to style the item internals, or go fully headless with useUpdateStatus() / useUpdatePriority(). Full worked example with the exact component names: Primitives → “custom dialog header with a custom dropdown.” Custom sidebar filters (V2): add your own filters + restyle the filter dropdown/panel. Configure which filters show (and add type:'custom' filters with your own options) via the filters prop; pick the layout with filterPanelLayout / filterOptionLayout; and fully restructure the filter dropdown/panel via the FilterContainer / FilterDropdown / FilterButton wireframe slots: Velt keeps the filtering logic. Full details: Component config → “Sidebar filters.” Collapsed→expanded composer (wireframe layout + CSS state class). Render a collapsed input and an expanded one in the composer slot; let Velt’s state classes switch them:
(velt-composer-open / velt-comment-dialog--no-comments / velt-composer-edit-mode are the switches.) Make the sidebar/list take available height and scroll (wireframe/primitive layout + CSS over Velt’s internals: the tricky one). Velt’s own internal container elements keep their default styles and sit between your layout and the scrollable list. If any link in that flex chain lacks min-height:0 / flex:1 / height:100%, the list grows past the panel and scrolling silently breaks. Inspect the Velt internal element and force the chain (usually !important):
Rule: min-height:0 (+ flex:1 / height:100%) must hold on every element from your wrapper down to the scroll container: including Velt’s internal ones. One missing link kills the scroll. Re-test that scrolling actually works. Remove leftover default styling a wireframe didn’t replace (wireframe + CSS !important). Wireframing a slot doesn’t strip all of Velt’s surrounding default styles: borders, padding, backgrounds, fixed widths, popover chrome often remain. Inspect → find the velt-* class → override:
Treat “inspect → find class → override” as part of every wireframe pass, not a failure. Wrap absolutely-positioned pieces in a position: relative container (primitive/wireframe mount + CSS). Some Velt pieces (dialog/pin/overlays, reaction pins) render with position: absolute. If a mounted primitive or wireframe appears in the wrong place (top-left, escaping its box), give its parent position: relative so the absolute child anchors to it:
The most common “why is my dialog/pin floating in the wrong spot?” fix.