← Neumo Design System docs

Demo build log — Vue (UnclaimedProperty.com)

What this is. A step-by-step account of rebuilding the UnclaimedProperty.com waitlist page with the Neumo design system in Vue, this time consuming the framework-agnostic web components (@kofile/gds-wc) rather than React components. It documents the prompts, the tools called at each step, the Vue-specific integration work, and the Playwright snapshots + neumo MCP audits used to confirm token alignment.

The finished app lives at examples/vue-unclaimed-property.

View the live demo — the finished page, hosted.

The setup

Target design Figma UnclaimedProperty.comunclaimedproperty-homepage (node-id=4-5610)
Framework Vue 3 (<script setup> + TypeScript) on Vite
Design system @kofile/gds-wc web components + @kofile/gds-foundations tokens
Icons / font @phosphor-icons/web + @fontsource-variable/source-sans-3 (bundled — no external requests)
AI tooling Claude Code + neumo MCP server (audit-tokens / audit-spacing / audit-typography) + Figma toolkit (read the design) + Playwright (screenshot + diff loop)
Sibling demos React (@kofile/gds-react) and .NET build the same page

The point of three demos: one design, one set of tokens, three idiomatic implementations. This Vue build is the web-component path — the same <neumo-*> custom elements the .NET demo uses, driven from a Vue single-file component.

A note on the exercise. In real life you rarely rebuild the same app in another framework, so the interesting part here isn't the design work (that was settled in the React and .NET builds) — it's how you consume web components idiomatically in Vue, and confirming token alignment holds across the stack. The prompts are essentially the React ones with "Vue" swapped in; the Vue-specific findings are what this log focuses on.


Step 0 — Read the target from Figma

Prompt: "Rebuild this interface — unclaimedproperty-homepage, node 4-5610, in Vue using our web components."

First, the connection. Reading a design into code needs a live Figma↔code link — the Neumo Toolkit + figma-console-mcp bridge. Set it up once via Connect Figma to code.

The design is the same frame the other two demos target — a dark hero (eyebrow → two-line headline with purple accent words → lede → three stats), a white signup card, and a light trust-bar footer:

The Figma design intent — unclaimedproperty-homepage (node 4-5610)

Because the .NET demo already rebuilt this exact frame with the same web components, its Index.cshtml markup and token-aligned site.css are a direct blueprint — the copy, structure, and styles are shared so all three demos match. The Vue build reuses that markup/CSS and focuses on the Vue integration.


Step 1 — Learn the real web-component API (don't guess)

Prompt: "What's the real @kofile/gds-wc API — element names, attributes, events — for the input, select, button, label, field-message?"

Tool used — the codebase (custom-elements.json + component source), which the neumo MCP server also exposes. The web-component contract is quite different from the React one, and three details shape the whole Vue integration:


Step 2 — Scaffold the Vue app (the integration work)

Prompt: "Scaffold a Vite + Vue 3 + TS app consuming the real published @kofile/gds-wc + @kofile/gds-foundations, latest versions, best practices."

Created examples/vue-unclaimed-property/. Four Vue-specific integration points are worth calling out — this is the actual value of the Vue demo:

1. Tell Vue's compiler that neumo-* are custom elements (vite.config.ts) — otherwise Vue tries to resolve them as Vue components and warns:

vue({
  template: {
    compilerOptions: { isCustomElement: (tag) => tag.startsWith("neumo-") },
  },
});

2. Register the components + tokens once, at the entry (src/main.ts):

import "@kofile/gds-foundations/themes"; // tokens (re-exports the branded theme)
import "@kofile/gds-wc";                 // side-effect: registers every <neumo-*>

There is no per-component subpath export — the single @kofile/gds-wc import registers all elements.

3. Bind the select's options as a DOM property, not an attribute. Because <neumo-select> reads an options array property, Vue's .prop modifier is required — a plain :options (or child <neumo-option> tags) leaves the menu empty:

<neumo-select :options.prop="stateOptions" placeholder="Select state..." />

4. Read values / listen for events like DOM. Vue 3.5 useTemplateRef gives the element; .value reads it; the button's custom event binds with @:

<neumo-input ref="emailEl" @ds-input-input="clearEmailError" />
<neumo-button @neumo-button-click="onSubmit">…</neumo-button>

The layout (src/app.css) and markup are ported from the .NET demo's token-aligned site.css — same <neumo-*> tags, same wrapper classes, so the stylesheet is framework-agnostic and needed no Vue-specific changes.

Font + icons are bundled, not CDN-loaded (@fontsource-variable/source-sans-3

Node/tooling note: the environment runs Node 20.15, so the toolchain is Vue 3.5 + Vite 6 + @vitejs/plugin-vue 6 (Vite 7/8 require Node ≥ 20.19).


Step 3 — Pixel-match loop

Prompt: "Run it, screenshot with Playwright, and diff against the Figma frame. Then run every value through the neumo MCP server — color, spacing, typography."

Pass 1 — first render

npm install (private packages resolve via the npm token) → npm run dev → Playwright screenshot. Because the markup + token-aligned CSS were ported from the web-component reference, the first render came up essentially pixel-perfect — dark hero, sunflower eyebrow, purple accent words, stats, white card with the <neumo-*> form, primary button with the magnifying-glass icon, and the trust footer, with no runtime errors and the isCustomElement config keeping Vue quiet.

The finished Vue build, matching the design

The functional integrations also verified clean: the state select populated its 6 options via :options.prop, and submitting the form (via the neumo-button-click event) showed the success state.

The waitlist success state after submit

Pass 2 — color

Tool used — neumo MCP audit-tokens on the stylesheet's color declarations. Result: 18 / 18 colors aligned (uses-token, delta 0) — hero --color-primary-1000, eyebrow --color-brand-accent, accent words --color-primary-400, lede/stat-label --color-neutral-400, and so on. The one raw value — the card box-shadow rgba(18,18,18,0.24)direct-matched --primitive-gray-black (#121212). Confirmed the resolved values in-browser via computed styles (hero #160a30, eyebrow #efe745, title #ffffff).

Real delta found (a component gap, not a demo bug): the Figma design shows the inputs with a light-gray fill (data-alt-background), but the rendered <neumo-input> stays white — the gray-fill CSS is gated on a data-mode the composed component doesn't propagate to its internal input. The .NET reference renders white for the same reason, so the Vue build matches the reference; the design's gray input fill is an upstream @kofile/gds-wc issue to file, not something to hack around in the demo.

Pass 3 — spacing

Tool used — neumo MCP audit-spacing. Result: 27 / 29 on tokens — every padding / gap / margin maps to a spacing token (--spacing-2xl, -xl, -lg, -sm, -xs, -2xs, --primitive-40/48/32/80), and the card radius 16px is on-scale (--borderRadius-xl). The only two off-scale values are the design's literal column widths (715px hero copy, 651px card) — intentional layout dimensions from the Figma nodes, not spacing-scale candidates.

Pass 4 — typography

The type ramp is fully tokenized (--font-size-6xl 72 / --line-heights-6xl 80 title, -3xl stat value, -2xl card title, -lg lede, -md subtitle, -sm stat label, -xs eyebrow/fineprint/trust; --font-weights-bold/semibold/regular; --letter-spacing-*). Verified the resolved metrics in-browser (title renders 72px / line-height 80px / weight 720 / letter-spacing −1.8px).

MCP finding (worth fixing): unlike audit-tokens and audit-spacing, which have a uses-token bucket and correctly credit var(--…) usage, audit-typography doesn't recognize var() token references — it buckets a fully-tokenized ramp as off-scale-far / wrong-family. So it can't confirm a tokenized type ramp; you have to verify via computed styles. Making audit-typography understand var() token usage (like the other two auditors) is a good MCP follow-up.

Result: the Vue implementation matches the Figma frame and is built on real design tokens — color, spacing, radius, and type — with alignment confirmed through the MCP, and two genuine findings surfaced (the input data-alt-background component gap and the audit-typography var() blind spot) rather than papered over.


The prompt — same loop, Vue context

The token-alignment loop is the same repeatable prompt used for the React build (read the design's real token bindings with the Figma MCP; screenshot + diff with Playwright; run every hardcoded value through the neumo MCP — audit-tokens, audit-spacing, audit-typography — using the token where one exists, flagging create-token gaps where none does). See the React build log for the full annotated version. For a web-component rebuild the only additions are framework-integration checks:

When rebuilding in Vue with the web components, also verify:
  • isCustomElement is configured for the neumo-* prefix
  • array/object inputs use the .prop binding modifier (e.g. :options.prop)
  • custom events are wired (@neumo-button-click), values read via el.value
  • the tokens + component bundle are imported once at the entry
Then run the same audit-tokens / audit-spacing / audit-typography passes.

Where the MCP server + Figma toolkit show up