> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build gotchas: common traps and fixes

> Avoid common build-time mistakes when translating design specs into Velt UI customizations.

<Info>
  **Audience: AI coding agents** (and teams running them). This page is part of the deterministic agent workflow for building and verifying Velt UI customizations. Customizing by hand? Start with the [Approaches](/docs/ui-customization/styling) and the [feature overviews](/docs/ui-customization/features/other-features). This is the workflow the [UI Customization Plugin](/docs/get-started/ui-customization-plugin) runs for you.
</Info>

These are common failure modes when building Velt UI customizations from a design. Check them before assuming the SDK is broken.

## Inspecting

* **Inspect the LIVE rendered node, not the registry template.** A wireframe is cloned: the `velt-*-wireframe` custom-element tags are the hidden registry copy (0-size, empty). `document.querySelector('.vc-x')` may hit that copy. Always pick the element with `getBoundingClientRect().width > 0` (the visible clone) and read *its* classes/computed styles. Measuring the wrong node is how you "verify" something that's actually broken.

## Wireframe clone behavior

* **Some slots OVERWRITE their inner markup with their own label.** `ToggleReply` replaced custom `<svg/> + <span>Reply</span>` with a plain "Reply" text node; `CopyLink` did the same. **Two verified fixes:** (a) inject the icon via CSS `::before` (a data-URI SVG on your `.vc-*` class), which survives the clone; or (b) put the custom icon in a plain sibling wrapper *next to* the slot's `.Text` subslot, never inside the slot itself. If you use a different reply slot with nested children, live-verify the children survive adoption before certifying, overwrite behavior is per-slot.
* **Wireframe MARKUP changes need a FULL page reload to take effect, not just new wireframes.** CSS edits hot-reload fine, but the `<VeltWireframe>` registry is built at mount: changing an existing template's markup (e.g. switching the composer's send to a self-closing `ActionButton`, or swapping Cancel to a `VeltButtonWireframe`) **re-renders under Fast Refresh but does NOT re-register the template**: the browser keeps rendering the OLD wireframe, so your fix "doesn't work" until a hard reload. After ANY `*Wf.tsx` change: hard-reload (Cmd-Shift-R), re-auth, reopen, and verify in a freshly-loaded tab, never a hot-reloaded one. (This masked a *correct* composer-submit fix as "still broken".)
* **Container slots drop undeclared children**: declare the full child tree you intend to use.
* **`velt-if`/`velt-class` attributes on plain HTML elements NEVER fire (R28).** A `velt-if="…"` on a `<div>`/`<span>` (or a helper spread like `{...veltIf("…")}`) survives the clone as inert markup, the element renders unconditionally, the class never toggles, nothing errors. Directives resolve **only on Velt elements**. Gate custom HTML by wrapping it in `<VeltIf condition="{…}">`; toggle classes via `velt-class` on a Velt wireframe element or key CSS off Velt's own state classes (e.g. `.velt-composer-open`, the submit button's `:disabled`).

## Styling / scoping

* **Class CSS needs shadow off + `!important`** (R6/R9b). With `shadowDom={false}` the live classes are reachable; Velt's runtime CSS is high-specificity, so overrides need `!important`.
* **The page-mode composer renders the WHOLE dialog wireframe** (your `.vc-card` and all). It inherits the card chrome (border/shadow/resolve-icon) and crushes the input. **Fix:** scope the card chrome off in that context: `.velt-comment-dialog--page-mode-composer .vc-card { border:none; box-shadow:none; padding:0; background:transparent }`, leaving just the composer pill. (Alternatively give the page-mode composer its own variant via `pageModeComposerVariant`.)
* **Composer "active" state = the `.velt-composer-open` ancestor class** (focus/compose), not a "has-text" class (there is none). Style the send button: grey/disabled by default, dark/enabled under `.velt-composer-open`.
* **Avatar fill color is user-data-driven**, not CSS: "User 1" renders peach. To match a design that shows a flat dark avatar, override `.…s-user-avatar-initial-container { background }` + the initial color (this overrides per-user colors: a deliberate choice to flag).
* **The send-arrow indigo lives on an inner element** (`.velt-composer--input-button`), not the outer `.velt-composer--submit-button`: override the inner one.

## Composer actions, collapsed replies, resolved state

* **Cancel button: use `VeltButtonWireframe`, never a raw `<button>`.** The dialog/reply composer has no native Cancel slot.
  * Render Cancel as a `VeltButtonWireframe` (a Velt-owned button) and wire it in the host via `useVeltEventCallback("veltButtonClick")` to clear and collapse the composer.
  * A raw `<button onClick>` in wireframe markup does not run because it is cloned to plain DOM (R4). It breaks specifically in the **in-thread reply composer**: page mode can look fine while the reply composer is dead.
  * The host handler must scope to both composers, but **`commentAnnotation` does not distinguish them because both carry one**. The real distinguisher is the annotation's comment count: the **page-mode** composer fires with a fresh **draft** (`commentAnnotation.comments.length === 0`); a **reply** composer fires with the **existing thread** (`comments.length > 0`). Route the clear accordingly.
  * Key the Send button's enabled/dark state off the submit button's `:disabled` attribute rather than only `.velt-composer-open`; it tracks empty-vs-filled exactly.
* **Send button → leave `Composer.ActionButton` SELF-CLOSING; paint the arrow with CSS.** Injecting a child (`<svg>`/`<span>`) into `ActionButton` is dropped by the clone and can kill the native submit in the reply composer. Leave it empty so Velt renders the functional submit, then mask the up-arrow via CSS (`::after`/`mask` on the live button class: grey idle → dark under `.velt-composer-open`).
* **"Show N replies…" (MoreReply): the clone DROPS trailing text, so add the ellipsis in CSS.** Velt renders the text in `.velt-hidden-count`; the cloner drops every node after the last velt element, so the trailing `…` is gone.
  * Re-add it via `::after { content:'…' }` on `velt-comment-dialog-more-reply-text-internal`.
  * Gate the row on `:has(.velt-hidden-count)` (NOT `:not(:empty)`) so a fully-expanded thread (empty MoreReply) doesn't show a lone chevron.
  * Indent to the message column and draw the **rail-line continuation** (a 1px line in the avatar gutter joining the chevron to the next avatar); suppress Velt's default full-width divider.
  * **Collapse the EMPTY slots**: a single comment / fully-expanded thread renders an empty `velt-comment-dialog-more-reply-internal` AND empty `velt-comment-dialog-toggle-reply-internal` that still take vertical space *inside the card border*, leaving dead space below "Reply". Add `velt-comment-dialog-more-reply-internal:not(:has(.velt-hidden-count)), velt-comment-dialog-toggle-reply-internal:not(:has(…count…:not(:empty))) { display:none !important }` so only a populated "Show N replies" row occupies space (on open AND resolved cards).
* **Resolved card = muted AND no reply.** Per the design a resolved comment hides its reply affordance. Velt adds no `--resolved` class: detect resolved via the rendered unresolve button (`.velt-comment-dialog:has(velt-comment-dialog-unresolve-button-internal .vc-icon-btn)`), then mute colours/avatar AND `display:none` the reply + `velt-comment-dialog-toggle-reply-internal` + any reply composer. Leaving them shows empty bottom space.

## Interaction-transition traps (the surface moves DURING a click) (R27)

These don't show up in a static per-state capture; they only bite mid-interaction. The whole class: a layout/visibility decision keyed on a state that **changes at the worst moment**.

* **Send/Cancel "shifts down and never fires", visibility keyed on a TRANSIENT state.** The reply composer's "Reply" link was hidden via `.velt-composer-input-focused` (focus). Focus drops the instant the pointer leaves the input to press a button → the rule re-shows the "Reply" link → everything below shoves down by a line → the composer (Send/Cancel) jumps **out from under the cursor**, so the click lands on empty air. Symptom reads as "the button doesn't work"; the real bug is it **moved**. **Fix: anchor on a STABLE state** that holds through the whole interaction, hide the link via `.velt-comment-dialog--selected` (the card is open the entire time you click) instead of `…input-focused`. Same visual result, zero flicker, the target never moves. **Never key hide/show or re-layout on `:focus`/`.velt-composer-input-focused`/`:hover`/`:active`**, they flip on pointer-down. (Send-button *enabled/dark* may key off the submit's `:disabled`, that tracks empty-vs-filled, not pointer position, so it's safe.) Prove it: measure the target's box, blur the input (what the click does), re-measure → must be 0px, then do the real thing, type a reply, real-click Send, confirm it posts.
* **Flex `gap` reserves space for a 0-height/empty child → trailing whitespace below the card.** A card laid out as `display:flex; flex-direction:column; gap:16px` adds the 16px gap **even when the next child (the collapsed reply composer / empty more-reply host) is 0px tall but still present**. A `gap` can't tell "invisible" from "absent". **Fix: `gap:0` on the column and move the spacing to `margin-top` on the child that should be spaced**, a margin only takes effect when its element actually has height. Verify in the **collapsed/empty-composer** state by measuring body-bottom→card-bottom = 0 extra px, not just the populated state.
* **Gutter-pinned glyphs must be PINNED, not inline.** The "Show N replies" chevron belongs in the avatar gutter under the avatar (design), but drawn inline it flows next to the text and pushes right. Pin it: `position:absolute; left:4px` against the card's positioned ancestor, and draw it as a **thin** glyph via a CSS `mask` (a data-URI SVG), not a heavier default.

## Dropdowns / selected state

* **The minimal-filter `SelectedIcon` slot renders as ONE standalone, full-width, `opacity:0` element**: it does NOT auto-place a tick per row. **Fix:** hide that slot and put a ✓ on the actually-selected row via CSS: `.…content-item--selected .vc-filter-item::after { content:"✓"; margin-left:auto }`. (Gating with `VeltIf {isSelected}` does NOT work in this V1 sort/filter context: it resolves falsy.)
* **A right-edge-anchored dropdown can overflow the viewport** (the trigger sits at the sidebar's right edge). The content's default `left/right` may push a wide menu off-screen, clipping the tick. **Fix:** shift the menu into view (e.g. `transform: translateX(-Npx)` on the menu, or right-anchor it) so it opens leftward.

## Interaction driving (when verifying in the browser)

* **Velt triggers need a real pointer click; `element.click()` (JS) often won't fire the Angular handler.** Use a real click at the element's coordinates.
* **Velt auth or `documentsReady` can stall after reloads** (`useCurrentUser` doesn't emit, so nothing mounts and the `velt-*` count is 0). This is an environment block, not a build failure. Wait longer for the mount, recover with a fresh tab, or re-authenticate in the app. Triage app vs. build before blaming the customization.

## Brief/drive selectors must survive the BUILD (post-build stability)

Probe briefs and drive steps are authored at plan time, against the app's **pre-build** DOM, but the wireframe registration **replaces that DOM**. A selector keyed to default Velt markup or host structure that the wireframe supersedes matches nothing the moment the first registration renders, and every downstream drive/measure dies on a selector that "worked when I verified it."

**Key every brief/drive selector to something that survives the build:**

* a **contract wireframe tag** (`velt-*-wireframe`, the manifest `contract.parts` `selectorHint`s exist precisely for this),
* a **stable `velt-*` runtime class** (`.velt-comment-dialog--sidebar-mode`, `.velt-composer--submit-button`, …, verified in [`CSS classes`](/docs/ui-customization/reference/css-classes)),
* or your own **`.vc-*` classes** from the first-shot stylesheet (you control that markup, so those classes are guaranteed post-build).

Never a pre-build class, a default-slot structure the design replaces, or a host wrapper the reconciliation may neutralize. Content-anchored selectors (`:has-text("…")`) are fine **only** on top of a stable base selector, fixture text survives; structure doesn't.

## Wireframe-host semantics FLIP at registration (selectors AND CSS)

The moment a family's wireframe **registers**, the live DOM's shape changes, not just its content:

* The **live card itself starts carrying `--wireframe-host`**, and the live inner elements render as **`*-internal` tags** (e.g. `velt-comment-dialog-header-internal`), not the tag names you saw pre-registration.
* The **hidden 0-size registry twins disappear**, the pre-registration DOM had every wireframe tag twice (a hidden registry copy under `<velt-wireframe>` plus the live clone); post-registration there is one live tree.
* Consequently **any `:not(--wireframe-host)` selector written pre-registration silently stops matching**, it was excluding the live element's own new class. The same applies to selectors assuming the twin structure (`velt-wireframe > *`), and to drive/probe waits keyed to a pre-registration tag.

**Author selectors against the post-registration DOM**: contract wireframe tags, `*-internal` live tags, stable `velt-*` classes, or your own `.vc-*` classes, and after the first registration of each family, re-verify any selector written before it.
