Skip to main content
This is the process. The decision tree picks the layer; this page explains how to build the customization once that layer is chosen. Use it as a measured workflow: inspect the live rendered DOM, compare every mapped state against the design, and do not mark the surface done until every required element and behavior passes.

The one principle

Build in small patches, each made pixel-perfect before the next. Never build the whole design in one pass. A run that registers every wireframe + writes all the CSS at once and then “judges the surface at 90%” is the failure mode. Instead: one element → match it to the pixel → next element. Small, verified, compounding.

The pipeline: extract → map → build → measure

Fidelity is a data problem, not a judgment one. Don’t look-and-style; run this pipeline:
1

Extract

Produce the designSpec: exact spacing, sizing, radius, typography, colours, and the design’s exported icon SVGs (Extraction). Numbers are read, never eyeballed.
2

Map

For each element, decide the real slot, prop, variant, or icon from the manifest (Manifest): props-first (structure a prop produces is never CSS) and supply every mustSupply slot (the design’s SVG/text, never a Velt default).
3

Build

Execute that map: wireframe files, an icons/ file from the exported SVGs, host props set, and the designSpec’s exact cssDecls applied to the real classes.
4

Measure

Diff rendered computed styles against the designSpec, per element and per property (Verifying a customization); “looks close” is a FAIL.
Step 1 below is the design overview that feeds extract+map; Step 2 is build; the measured loop closes it.

Step 1: Design overview (read EVERY frame before building anything)

Go through the Figma frame by frame and write down, exhaustively:
  1. Which features/surfaces exist: e.g. a comments sidebar AND a pin dialog are two different surfaces; don’t miss one. List each.
  2. Per surface, every element: for a dialog: header (or none), thread card (avatar, name, time, message, mentions, reply/toggle), composer, etc. For a sidebar: header (title + filter), composer, list.
  3. Every state shown: collapsed-by-default vs expanded, empty placeholder, composer focused, composer expanded (long message → grows to a max-height then scrolls), autocomplete/@mention dropdown, hover (reveals resolve + kebab), selected, resolved (greyed + unresolve), filter dropdown open, options dropdown, toasts/tooltips, selected-filter tick. Note which state the user wants as the default.
  4. Exact tokens: colors (incl. mention/link color), type sizes/weights/line-height, spacing, radius, shadows, icon set.
  5. What is NOT supported by default: explicitly list every piece the SDK can’t do out of the box, and the plan for each:
    • achievable via a prop/config/wireframe slot → note which,
    • needs primitives/headless + custom logic → confirm the data model actually supports it (Data models) before claiming feasible; if a field/event doesn’t exist, it’s a real gap → document it + suggest the closest supported alternative (e.g. a “mentions” filter isn’t in the minimal filter → suggest assigned-to-me / involved),
    • needs an event-driven addition (e.g. a resolved toast, a link-copied tooltip) → clean; subscribe to the event (Events),
    • might feel hacky (e.g. a “Cancel” button the SDK has no slot for → clear the composer via JS) → document the approach and confirm with the user before shipping it: never ship a brittle hack silently (R0).
The output is a complete, per-surface, per-state checklist plus the unsupported-items list. This is what the implementation and verification steps work against.

Step 2: Implementation (one small patch at a time, to pixel-perfection)

Pick one surface (dialog or sidebar: dialog first is usually easiest). Then, within it: a. Build the structure, slot by slot. Declare the wireframe tree for that surface (full container trees: containers drop undeclared children). Get it rendering before styling. a2. Supply every slot + set props first. Before styling, fill every mustSupply slot from your implementation map (the design’s exported SVG icon, the exact label, the explicit menu items: never a Velt default), and set the host props that produce structure. Only set props that a design cue justified (R24). Don’t add a feature flag the design doesn’t show (visibilityOptions adds an unwanted banner). Structure from a prop is never built in CSS. a3. Reconcile layers: paint once on the box-matched owner (R22/R23). A Figma node is one rectangle; the DOM is a nested stack. Before per-element styling, inspect each painted node in the live rendered output and apply the design’s box-painting (background, border, radius, padding) to the owner, meaning the layer whose box matches the design node: your .hw-* element or the leaf Velt class. For every co-box wrapper that duplicates painting, reset only the offending box-painting property (padding:0, background:transparent, and similar, with !important). Never reset functional CSS such as flex, overflow, or position. This flattens redundant Velt wrappers (velt-sidebar-container, app-comment-sidebar-list, the page-mode-composer dialog, and similar) so the design’s gutter, background, and border land once. Then compose each group from the layout relationships: header row = [Avatar | Name·Time·Edited·Unread | actions], message under the header at the same left edge, actions top-right hover-reveal. Derive inner spacing from the designSpec gaps, never from stacked padding. b. Style element by element: apply the EXACT numbers (this is the core). Style the slot’s correct role (R23): for a menu/popup apply the chrome to the content slot, never the dropdown container (it wraps the trigger → a 210px box) or the trigger (style that as the 24px icon); scope mention CSS to the message’s mentionScope only. For each element, in a small patch:
  1. Render the surface in the live app (shadow off so class CSS reaches), with seeded data so the element actually shows.
  2. Inspect the LIVE rendered element (not the hidden registry template: those *-wireframe tags are 0-size copies) to find its real Velt class (.velt-thread-card--name/--time/--message, .s-user-avatar-container, .velt-composer--submit-button, …).
  3. Apply the mapped cssDecls for that element (the exact numbers from the designSpec) to that class, overriding Velt’s default with !important (R9b). Don’t re-measure or eyeball: the numbers are already exact in the designSpec.
  4. Measure the rendered computed style against the designSpec (Delta E below 2, ±1px) and record a delta table; close any failing rows. Then move to the next element.
c. Interactivity & states. Then handle hover/active states: e.g. resolve + kebab hidden by default, revealed on card hover; keep them visible while the options dropdown is open (the “active” case: hover OR a button under action). Fix empty-control gaps (display:none on an empty unresolve button). Composer collapsed→expanded→max-height→scroll. Hidden scrollbars (scrollbar-width:none). d. Test every feature functionally, as a real user. Edit, delete, copy link, add reply, delete main/reply, resolve/unresolve, draft, edited badge, tagging one/many users. Keep fixing until it’s 100% right functionally and visually. (Draft/edited badges may not be in the design: keep them, themed to match.) e. Next surface. Repeat a-d for the sidebar (header title + filter, page-mode composer, list). f. The unsupported items, last. Implement the event-driven ones (toast, tooltip) cleanly; confirm the hacky ones with the user; document the genuine gaps with their suggested alternatives.

The loop is block by block

Every Figma frame or state is one block. The design’s frames are the checklist, so “stopped at the happy path” is incomplete when another required state exists. Finish each block to a true match before starting the next:
for each block (composer-default first, then states):
   build/patch the block's elements  (Step 2 above: structure → supply slots → reconcile → exact numbers)
   SEED + DRIVE the block's state in-app  (block.drive/fixture: type/click/hover/resolve; wait for drive.assert)
   CAPTURE device-res evidence
   MEASURE:  visual diff with text masked  (chrome-only; any significant named region ⇒ FAIL)
          + computed-style comparison  (exact style Delta E below 2 / ±1px + layout box/gap/relation)
           + layer reconciliation (R22/R23) + behavior contract check (R25)
   iterate until: no significant visual region ∧ delta tables empty ∧ contract ok  → block PASS
   advance only on PASS
terminate only when the complete checklist review passes for ALL blocks
Why this is mechanical: text has a Figma-vs-Chrome glyph-rendering floor, so visual comparison should focus on chrome, missing elements, extra elements, and wrong structure. Computed-style comparison owns exact color, size, and position. Neither check is enough alone; both must be clean. Keep evidence collection separate from the verdict: collect the data first, then decide whether the block passed. A retry is only progress if the significant-region or failing-diff count drops. See the traps in Build gotchas.