Match Figma specs when implementing or reviewing QBDS UI — tokens, layout, all variants/states, Code Connect, demos, registry. Use when implementing, updating, or reviewing a QBDS component against a Figma URL/node or the library spec. Triggers — "implement this Figma component", "match the Figma spec", "figma parity", "build this component from Figma", or a figma.com URL alongside work in src/components/ui/, demos, registry.json, or code-connect/.
Scope: Run this workflow only for Figma-driven implement/review of QBDS components. Skip for unrelated tasks (deps, CI, docs, refactors with no design change). Figma MCP output is reference only — still run the full workflow below.
If no Figma URL/node is provided, ask for it (or use the team’s internal/shared link).
src/styles/globals.css — CSS variables and @theme inline; trace utilities back to semantics (e.g. bg-fill-* → --color-fill-* → --fill-*). Verify light and dark (.dark); use -inverse on dark/accent surfaces.
Spacing scale — gap-1 / p-1 = 4px, gap-2 / p-2 = 8px, gap-3 / p-3 = 12px. No gap-[Npx], text-[#…], bg-[#…], or primitives (slate-*, mist-*) in components.
Match Figma tokens via docs/TOKENS.md (Design name + Tailwind utility). CSS semantics in globals.css: --text-*, --border-*, --fill-*, --surface-*, --status-*, --stateslayer-*, --elevations-*, --brand-accents-* (each has a -inverse form where the spec uses inverse surfaces). In components use Tailwind utilities from TOKENS.md — e.g. text-fg-* (→ --color-fg-* → --text-*), border-stroke-* (→ --color-stroke-* → --border-*), bg-fill-* — not raw --fg-* / --stroke-* or primitives.
Feedback / status colour (do not flag text-status-*): Figma may bind feedback copy to Text/Error, Text/Warning, etc. In QBDS code the established theme utilities are text-status-error, text-status-warning, text-status-success, text-status-information (same family as fills/borders via --color-status-*). That mapping is correct and intentional — match sibling demos (field, input, alert, …). Do not treat text-status-* on feedback, counters, or required markers as a token drift, and do not rewrite them to text-error / text-warning / text-success / text-fg-error. Use border-stroke-status-* / bg-status-* for control chrome as today.
Repo patterns (match siblings, do not reinvent)
Component: src/components/ui/<name>.tsx — primitive + cva + context + data-slot; copy structure from the closest sibling, not from scratch. Base UI vs Radix: see Which primitive to use in AGENTS.md — the library is mid-migration, so check the imports in the file you are editing.
Demo: src/app/demo/[name]/ui/<name>.tsx — named exports + examples: DemoExample[] (canonical; createLegacyDemo is a legacy wrapper still exported by many existing demos — do not add it to new demos). Follow demos.md; covering a Figma axis does not require a separate example per optional slot toggle.
Registry: registry.json + npm run registry:build → public/r/.
Icons: <IconShell> + <Icon icon="snake_case" />; shell owns colour — pair sizes with the parent control per sibling demos. When the parent surface flips (dropdown-open, toggle-on, inverse fill), re-check nested IconShell Type (neutral vs neutral-inverse) and State/opacity (primary vs secondary) — size alone is not enough.
Interactive states: match the nearest button-like sibling — state overlays, disabled fills, focus rings; do not let variant branches override size-level focus treatment.
Slots: follow composition.md export decision tree — SLOT → children of existing parts first; new export only when the tree says so. Mark seams with data-slot.
Field footer: FieldDescription (helper) and FieldError (feedback) are mutually exclusive — one per field. Error/invalid → FieldError only; otherwise FieldDescription when helper is shown. Never both in demo or Code Connect.
Horizontal Field lists: Field is w-full; horizontal labels use flex-auto. Inside horizontal CheckboxGroup / RadioGroup, that stretches items and makes density gaps look wrong. Shrink-wrap on the group ([&>[data-slot=field]]:w-auto + label flex-none). Do not change Field globals.
Primitive composition: The primitive that owns focus and keyboard behavior must be the rendered element — Base UI render, or Radix asChild on the outer primitive wrapping the styled QBDS sub-component (not the reverse). Verify keyboard navigation in the demo.
Public API: follow composition.md. Figma parity verifies styling of exported parts, not that every Figma layer becomes an export. Every exported sub-component needs a demo example and at least one test, or remove it from the public API — do not export Figma-internal frames to satisfy this rule.
Size drift red flag: the same axis on both data-* and React Context without a documented reason in the alignment table.
Field-composed controls (input, textarea, select, date/time pickers, …): Figma nests Elements/ instances (Label, Help-Text, Status-Messages, Characters-Counter). Match each slot per size × state in the demo — copy the per-size fieldConfig pattern from the nearest sibling (e.g. input.tsx). Do not assume shared Field* defaults match the parent set’s spec.
Workflow (run in order)
0 — Code Connect (do NOT treat as source of truth)
code-connect/<name>.figma.ts (or legacy .figma.tsx) is a downstream artifact — it can be wrong or stale. Do not seed the alignment table from it and do not "verify" Figma against it.
Source of truth = Figma (get_metadata, get_variable_defs, get_screenshot) + the real React API (src/components/ui/<name>.tsx).
Build the alignment table from those first, then check the mapping against it (see step 4). Any mismatch — enum values, size names, variant→prop mapping — is a Code Connect bug to fix, not a spec to follow.
If Figma has a dropdown-open (or similar expanded) state and Code Connect maps it to false, omits open fill/icon changes, or documents a static IconShell type with no open override — that is a mapping bug to flag, not proof that open equals enabled.
To create or update a mapping, use the code-connect skill — it owns the mechanics and QBDS conventions.
When calling get_design_context, pass disableCodeConnect: true so the response cannot seed the spec with a stale mapping.
For field-composed controls: derive errorClass (and warning/success/info) the same way as labelClass / descClass — pass explicit className on <FieldError> when Figma typography varies by size.
1 — Structure & variants
Component description (Figma) — On the component set root (not a single variant instance), read the designer-written description in Dev Mode, or from get_design_context on that node (follow component documentation links and design annotations in the response when present). QBDS descriptions often state the default layout and which variant axes exist (e.g. “Defaults to unboxed horizontal… also supports boxed, vertical, …”). Treat this as the narrative source of truth for defaults and scope before inferring from one frame.
get_metadata (or Dev Mode) on the component set — list every variant, boolean, text, and SLOT property; note which property values are the set’s default selections.
Prop naming: props.md — map Figma sizes to React vocabulary (reg→default, xlg→xl, …); variant uses default not primary.
Build an alignment table from Figma + React API (never from Code Connect); flag asymmetric coverage (Figma-only or code-only values). The Code Connect column records what the mapping claims — a mismatch there is a mapping bug to fix, not a reason to change the spec:
Axis
Figma values
React values
Code Connect (verify)
Aligned?
Notes
Variant / type
…
…
…
Size
…
…
…
Other layout axes
…
…
…
State (if on set)
…
…
…
Field slots
Label, Help, Status, Counter
FieldTitle, FieldDescription, FieldError, counter
…
Per size — see below
SLOT props
…
children / sub-components
…
Sub-components
…
exports
…
If a Code Connect mapping exists, confirm its enum values, size names, and variant→prop mapping match the Figma + React columns above. Fix the mapping (via the code-connect skill) when it drifts.
Field chrome table (required when Figma nests Elements/* or booleans showLabel, showHelpText, showFeedbackMessage, showCounter): inspect each text slot at sm, reg/default, and lg (and error/disabled states). Shared primitives (FieldError, FieldDescription) often ship generic defaults — override in demo/Code Connect when Figma differs.
Slot
Figma node
sm (type + color)
default
lg
Code target
Label
Elements/Label
…
…
…
fieldConfig.*.label
Helper
Elements/Help-Text
…
…
…
fieldConfig.*.description
Feedback
Elements/Status-Messages
…
…
…
fieldConfig.*.error (etc.)
Counter
Elements/Characters-Counter
…
…
…
counter sub-component
Red flags: label/description are per-size in demo but error/feedback is not; bare <FieldError> with no className on a sized field set; wrong paragraph-* (e.g. paragraph-regular-primary when Figma shows Paragraph/Large-Primary). Not a red flag:text-status-* on feedback / counter / required * (see Text vs status note above).
Defaults & demos:
Defaults: Figma description, cva defaults, and registry.json must agree.
Demos: Start examples[0] simple — developer's choice, not necessarily the Figma default. Cover every alignment-table row across examples[]. Include at least one error/feedback example per size when typography differs.
2 — Tokens (every distinct variant × state)
Use get_variable_defs on representative nodes: at minimum enabled, hover, focus, pressed, disabled, plus every other Figma state enum value when present — dropdown-open, toggle-on, selected/active/loading, etc. Do not stop at focus when the set also has expanded/open cells. Map fill, text, stroke, elevation, radius, and state overlays per docs/TOKENS.md. Check light and dark. Flag: wrong -inverse prefix; raw hex; primitives; right hex but wrong token name. When Figma shows Text/Error (etc.) on feedback, accept code that uses text-status-error (etc.) — do not flag that as a name mismatch.
3 — Layout, spacing, typography & states
For each matrix cell (every meaningful variant combination), use get_design_context or Dev Mode — not only the root frame. For field sets, also pull context on nested Elements/ frames in that cell.
Property
Figma
Code
Height / min size
auto-layout
size-*, min-h, padding + line-height
Padding / gap
spacing variables
p-*, gap-* on the scale above
Icon box
icon frame size + IconShell Type/State
IconShell size + type + opacity; re-read on open/toggle when parent fill flips
Separators / attached spacers
layout on group
avoid double gap; only between items
Typography (control)
text style name
matching cta-* / paragraph-* utility
Typography (field feedback)
Paragraph/* + Text/Error (etc.)
per-size paragraph-* + text-status-error (etc.) — verify each size, not control default
Underline / link CTA
CTA/button-* vs CTA/button-link-* (also Paragraph *-Link)
see CTA button vs button-link below
CTA button vs button-link (underline) — mandatory per variant × state
Figma text styles encode underline. Do not assume one underline rule for all variants.
Figma text style (enabled / state cell)
Code expectation
CTA/button-01|02|03 (no -link) at enabled
No underline at rest. Underline only if hover / focus / pressed / open cells switch to CTA/button-link-* (e.g. group-hover:underline on label).
CTA/button-link-01|02|03 at enabled
Permanent underline at rest (cta-button-link-* or always-on underline on the label).
Disabled cell uses CTA/button-* while enabled used -link
Drop underline when disabled (disabled:…:no-underline).
Red flags
Shared hover-only underline wrapper for every variant while Figma ghost (or similar) binds CTA/button-link-* at enabled.
Always-on underline while Figma enabled cell is plain CTA/button-*.
Size axis only sets cta-button-* with no per-variant link/underline branch when Figma differs by type.
Record pass/drift for underline in the variant × state matrix (not only font size/weight).
Compound spacing: Derive spacing from Figma per variant cell, not from a single axis (e.g. “if boxed, always gap-2”). Size and shape often change gap/padding independently — document or test non-default cells when logic is non-obvious.
Spacing verification rules
Record pl and pr separately in the spacing table — never assume symmetric padding.
Do not infer padding from symbol bounding-box width or total component width.
Verify padding on the inner State-Overlays frame (Dev Mode or MCP codegen on the variant cell).
When Spacing/N tokens appear together (e.g. Spacing/8 + Spacing/12), map each to its side; do not dismiss larger tokens as "internal only" without checking the overlay frame.
Shared cva ≠ shared spacing — if a sibling component (e.g. Tag vs TagToggle) diverges, document and fix per component file.
Interactive states (on controls inside the component):
State
Verify
Hover / pressed
overlay tokens (+ -inverse where spec uses inverse surfaces)
Focus
ring token, width, offset; fill/icons usually unchanged vs enabled — pull the focused Figma cell
Dropdown-open / expanded
Separate Figma cell from focus. Never copy focus-visible styles onto data-[state=open] without comparing both cells. Re-check nested IconShell Type + opacity when the parent surface flips (often dark/active-inverse + light/neutral icons). Red flag: shared focus-visible:… data-[state=open]:… classes with no focus-vs-open Figma comparison.
Toggle-on / selected
data-[state=*] / aria-* branches; icon tone if surface flips
Disabled
muted fill, disabled text, overlay
Loading
if defined in Figma
Error (field)
control border/fill (border-stroke-status-* / bg-status-*) and feedback text (text-status-*, per-size typography)
Composition components (split buttons, icon+menu triggers): verify open on the trigger segment that receives data-state=open, including nested IconShell — not only the parent cva file.
Visual pass (required):npm run dev on the demo vs Figma / get_screenshot per matrix cell. Fix or document any ≥2px mismatch.
Report a variant × state matrix: pass / drift (note ≥2px or wrong token).
Every Figma state value has a matrix cell; when both focused and dropdown-open exist, open ≠ focus verified (fill + nested IconShell type/opacity)
Code Connect (if present): enums, size names, variant→prop mapping verified against Figma + React API (not the reverse); drift fixed — including open/expanded not collapsed to enabled/false when Figma differs
Field chrome table (when Elements/* present): label, helper, feedback, counter — typography + color per size
Feedback / status copy uses theme utilities text-status-error|warning|success|information (Figma Text/* → text-status-* is OK)
Variant × state matrix: tokens + geometry per cell
CTA button-* vs button-link-* (underline) checked per variant × state — including ghost/link-like types at enabled
Light and dark where the component appears on both
Defaults aligned (Figma, cva, registry); demos start simple and cover the full alignment table
Compound spacing from per-cell Figma values (no undocumented single-axis shortcuts)
Horizontal padding verified as pl + pr per matrix cell (not width-inferred, not assumed symmetric)
Shared styling helpers checked per component when spacing differs
Exported sub-components: demo + test, or not exported
Composed primitives: correct asChild direction; keyboard nav verified
Extract node-id from the URL, run the workflow above, and compare to the nearest sibling in src/components/ui/.
1---2name: figma-parity3description: Match Figma specs when implementing or reviewing QBDS UI — tokens, layout, all variants/states, Code Connect, demos, registry. Use when implementing, updating, or reviewing a QBDS component against a Figma URL/node or the library spec. Triggers — "implement this Figma component", "match the Figma spec", "figma parity", "build this component from Figma", or a figma.com URL alongside work in src/components/ui/, demos, registry.json, or code-connect/.4---56# Figma ↔ code parity (QBDS)78**Scope:** Run this workflow only for Figma-driven implement/review of QBDS components. Skip for unrelated tasks (deps, CI, docs, refactors with no design change). Figma MCP output is reference only — still run the full workflow below.910If no Figma URL/node is provided, ask for it (or use the team’s internal/shared link).1112## Sources of truth (read before styling)13141. **[docs/TOKENS.md](../../../docs/TOKENS.md)** — semantic Tailwind utilities; design-name column maps to Figma variables.152. **[src/styles/globals.css](../../../src/styles/globals.css)** — CSS variables and `@theme inline`; trace utilities back to semantics (e.g. `bg-fill-*` → `--color-fill-*` → `--fill-*`). Verify **light and dark** (`.dark`); use `-inverse` on dark/accent surfaces.163. **Spacing scale** — `gap-1` / `p-1` = 4px, `gap-2` / `p-2` = 8px, `gap-3` / `p-3` = 12px. No `gap-[Npx]`, `text-[#…]`, `bg-[#…]`, or primitives (`slate-*`, `mist-*`) in components.1718Match Figma tokens via **[docs/TOKENS.md](../../../docs/TOKENS.md)** (Design name + Tailwind utility). CSS semantics in `globals.css`: `--text-*`, `--border-*`, `--fill-*`, `--surface-*`, `--status-*`, `--stateslayer-*`, `--elevations-*`, `--brand-accents-*` (each has a `-inverse` form where the spec uses inverse surfaces). In components use Tailwind utilities from TOKENS.md — e.g. `text-fg-*` (→ `--color-fg-*` → `--text-*`), `border-stroke-*` (→ `--color-stroke-*` → `--border-*`), `bg-fill-*` — not raw `--fg-*` / `--stroke-*` or primitives.1920**Feedback / status colour (do not flag `text-status-*`):** Figma may bind feedback copy to **`Text/Error`**, **`Text/Warning`**, etc. In QBDS code the established theme utilities are **`text-status-error`**, **`text-status-warning`**, **`text-status-success`**, **`text-status-information`** (same family as fills/borders via `--color-status-*`). That mapping is **correct and intentional** — match sibling demos (`field`, `input`, `alert`, …). Do **not** treat `text-status-*` on feedback, counters, or required markers as a token drift, and do **not** rewrite them to `text-error` / `text-warning` / `text-success` / `text-fg-error`. Use `border-stroke-status-*` / `bg-status-*` for control chrome as today.2122## Repo patterns (match siblings, do not reinvent)2324- **Component**: `src/components/ui/<name>.tsx` — primitive + `cva` + context + `data-slot`; copy structure from the closest sibling, not from scratch. Base UI vs Radix: see **Which primitive to use** in [AGENTS.md](../../../AGENTS.md) — the library is mid-migration, so check the imports in the file you are editing.25- **Demo**: `src/app/demo/[name]/ui/<name>.tsx` — named exports + `examples: DemoExample[]` (canonical; `createLegacyDemo` is a legacy wrapper still exported by many existing demos — do not add it to new demos). Follow [demos.md](../../../docs/qbds-react-components/demos.md); covering a Figma axis does not require a separate example per optional slot toggle.26- **Registry**: `registry.json` + `npm run registry:build` → `public/r/`.27- **Tests**: `src/tests/<name>.test.tsx` — a11y roles, `data-*` attrs, interaction; assert key variant outputs where helpers exist.28- **Icons**: `<IconShell>` + `<Icon icon="snake_case" />`; shell owns colour — pair sizes with the parent control per sibling demos. When the parent surface flips (dropdown-open, toggle-on, inverse fill), re-check nested IconShell **Type** (`neutral` vs `neutral-inverse`) and **State**/opacity (`primary` vs `secondary`) — size alone is not enough.29- **Typography**: globals utilities (`cta-*`, `paragraph-*`, …) — not hand-rolled `text-sm` / `font-*` / `leading-*`.30- **Interactive states**: match the nearest button-like sibling — state overlays, disabled fills, focus rings; do not let variant branches override size-level focus treatment.31- **Slots**: follow [composition.md](../../../docs/qbds-react-components/composition.md) export decision tree — SLOT → children of existing parts first; new export only when the tree says so. Mark seams with `data-slot`.32- **Field footer**: `FieldDescription` (helper) and `FieldError` (feedback) are **mutually exclusive** — one per field. Error/invalid → `FieldError` only; otherwise `FieldDescription` when helper is shown. Never both in demo or Code Connect.33- **Horizontal Field lists**: `Field` is `w-full`; horizontal labels use `flex-auto`. Inside horizontal `CheckboxGroup` / `RadioGroup`, that stretches items and makes density gaps look wrong. Shrink-wrap on the group (`[&>[data-slot=field]]:w-auto` + label `flex-none`). Do not change Field globals.34- **Primitive composition**: The primitive that owns focus and keyboard behavior must be the **rendered** element — Base UI `render`, or Radix `asChild` on the outer primitive wrapping the styled QBDS sub-component (not the reverse). Verify keyboard navigation in the demo.35- **Public API**: follow [composition.md](../../../docs/qbds-react-components/composition.md). Figma parity verifies styling of exported parts, not that every Figma layer becomes an export. Every exported sub-component needs a demo example and at least one test, or remove it from the public API — do not export Figma-internal frames to satisfy this rule.36- **Size drift red flag**: the same axis on both `data-*` and React Context without a documented reason in the alignment table.37- **Field-composed controls** (input, textarea, select, date/time pickers, …): Figma nests **Elements/** instances (Label, Help-Text, Status-Messages, Characters-Counter). Match **each slot** per size × state in the demo — copy the per-size `fieldConfig` pattern from the nearest sibling (e.g. `input.tsx`). Do **not** assume shared `Field*` defaults match the parent set’s spec.3839## Workflow (run in order)4041### 0 — Code Connect (do NOT treat as source of truth)4243`code-connect/<name>.figma.ts` (or legacy `.figma.tsx`) is a **downstream artifact — it can be wrong or stale.** Do not seed the alignment table from it and do not "verify" Figma against it.4445- **Source of truth = Figma** (`get_metadata`, `get_variable_defs`, `get_screenshot`) **+ the real React API** (`src/components/ui/<name>.tsx`).46- Build the alignment table from those first, **then** check the mapping against it (see step 4). Any mismatch — enum values, size names, variant→prop mapping — is a **Code Connect bug to fix**, not a spec to follow.47- If Figma has a `dropdown-open` (or similar expanded) state and Code Connect maps it to `false`, omits open fill/icon changes, or documents a static IconShell type with no open override — that is a **mapping bug to flag**, not proof that open equals enabled.48- To create or update a mapping, use the **`code-connect`** skill — it owns the mechanics and QBDS conventions.49- When calling `get_design_context`, pass **`disableCodeConnect: true`** so the response cannot seed the spec with a stale mapping.5051For field-composed controls: derive **`errorClass`** (and warning/success/info) the same way as **`labelClass`** / **`descClass`** — pass explicit `className` on `<FieldError>` when Figma typography varies by size.5253### 1 — Structure & variants54551. **Component description (Figma)** — On the **component set** root (not a single variant instance), read the designer-written **description** in Dev Mode, or from `get_design_context` on that node (follow **component documentation** links and **design annotations** in the response when present). QBDS descriptions often state the **default layout** and which variant axes exist (e.g. “Defaults to unboxed horizontal… also supports boxed, vertical, …”). Treat this as the narrative source of truth for defaults and scope before inferring from one frame.562. `get_metadata` (or Dev Mode) on the component set — list every variant, boolean, text, and SLOT property; note which property values are the set’s **default** selections.573. Read `src/components/ui/<name>.tsx` — `cva` keys, props, `data-slot`, exports.584. Prop naming: [props.md](../../../docs/qbds-react-components/props.md) — map Figma sizes to React vocabulary (`reg`→`default`, `xlg`→`xl`, …); `variant` uses `default` not `primary`.595. Build an alignment table from **Figma + React API** (never from Code Connect); flag **asymmetric** coverage (Figma-only or code-only values). The **Code Connect** column records what the mapping claims — a mismatch there is a mapping bug to fix, not a reason to change the spec:6061| Axis | Figma values | React values | Code Connect (verify) | Aligned? | Notes |62| ----------------- | ---------------------------- | ------------------------------------------------------- | --------------------- | -------- | ------------------------ |63| Variant / type | … | … | … | | |64| Size | … | … | … | | |65| Other layout axes | … | … | … | | |66| State (if on set) | … | … | … | | |67| Field slots | Label, Help, Status, Counter | `FieldTitle`, `FieldDescription`, `FieldError`, counter | … | | Per **size** — see below |68| SLOT props | … | children / sub-components | … | | |69| Sub-components | … | exports | … | | |7071If a Code Connect mapping exists, confirm its enum values, size names, and variant→prop mapping match the Figma + React columns above. Fix the mapping (via the `code-connect` skill) when it drifts.72736. **Field chrome table** (required when Figma nests Elements/\* or booleans `showLabel`, `showHelpText`, `showFeedbackMessage`, `showCounter`): inspect **each text slot** at sm, reg/default, and lg (and error/disabled states). Shared primitives (`FieldError`, `FieldDescription`) often ship generic defaults — override in demo/Code Connect when Figma differs.7475| Slot | Figma node | sm (type + color) | default | lg | Code target |76| -------- | --------------------------- | ----------------- | ------- | --- | ---------------------------- |77| Label | Elements/Label | … | … | … | `fieldConfig.*.label` |78| Helper | Elements/Help-Text | … | … | … | `fieldConfig.*.description` |79| Feedback | Elements/Status-Messages | … | … | … | `fieldConfig.*.error` (etc.) |80| Counter | Elements/Characters-Counter | … | … | … | counter sub-component |8182**Red flags:** label/description are per-size in demo but error/feedback is not; bare `<FieldError>` with no `className` on a sized field set; wrong `paragraph-*` (e.g. `paragraph-regular-primary` when Figma shows `Paragraph/Large-Primary`). **Not a red flag:** `text-status-*` on feedback / counter / required `*` (see Text vs status note above).83847. **Defaults & demos:**85 - **Defaults:** Figma description, `cva` defaults, and `registry.json` must agree.86 - **Demos:** Start `examples[0]` simple — developer's choice, not necessarily the Figma default. Cover every alignment-table row across `examples[]`. Include at least one **error/feedback** example per size when typography differs.8788### 2 — Tokens (every distinct variant × state)8990Use `get_variable_defs` on representative nodes: at minimum **enabled**, **hover**, **focus**, **pressed**, **disabled**, plus every other Figma `state` enum value when present — **`dropdown-open`**, **`toggle-on`**, selected/active/loading, etc. Do not stop at focus when the set also has expanded/open cells. Map fill, text, stroke, elevation, radius, and state overlays per **[docs/TOKENS.md](../../../docs/TOKENS.md)**. Check **light and dark**. Flag: wrong `-inverse` prefix; raw hex; primitives; right hex but wrong token name. When Figma shows **`Text/Error`** (etc.) on feedback, accept code that uses **`text-status-error`** (etc.) — do not flag that as a name mismatch.9192### 3 — Layout, spacing, typography & states9394For **each matrix cell** (every meaningful variant combination), use `get_design_context` or Dev Mode — not only the root frame. For field sets, also pull context on **nested Elements/** frames in that cell.9596| Property | Figma | Code |97| ----------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |98| Height / min size | auto-layout | `size-*`, `min-h`, padding + line-height |99| Padding / gap | spacing variables | `p-*`, `gap-*` on the scale above |100| Icon box | icon frame size + IconShell Type/State | `IconShell` size + **type** + opacity; re-read on open/toggle when parent fill flips |101| Separators / attached spacers | layout on group | avoid double gap; only between items |102| Typography (control) | text style name | matching `cta-*` / `paragraph-*` utility |103| Typography (field feedback) | Paragraph/\* + Text/Error (etc.) | per-size `paragraph-*` + `text-status-error` (etc.) — verify **each size**, not control default |104| Underline / link CTA | `CTA/button-*` vs `CTA/button-link-*` (also Paragraph `*-Link`) | see **CTA button vs button-link** below |105106**CTA button vs button-link (underline) — mandatory per variant × state**107108Figma text styles encode underline. Do **not** assume one underline rule for all variants.109110| Figma text style (enabled / state cell) | Code expectation |111| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |112| `CTA/button-01\|02\|03` (no `-link`) at **enabled** | No underline at rest. Underline only if **hover / focus / pressed / open** cells switch to `CTA/button-link-*` (e.g. `group-hover:underline` on label). |113| `CTA/button-link-01\|02\|03` at **enabled** | Permanent underline at rest (`cta-button-link-*` or always-on `underline` on the label). |114| Disabled cell uses `CTA/button-*` while enabled used `-link` | Drop underline when disabled (`disabled:…:no-underline`). |115116**Red flags**117118- Shared hover-only underline wrapper for every variant while Figma **ghost** (or similar) binds `CTA/button-link-*` at **enabled**.119- Always-on underline while Figma enabled cell is plain `CTA/button-*`.120- Size axis only sets `cta-button-*` with no per-variant link/underline branch when Figma differs by `type`.121122Record pass/drift for underline in the variant × state matrix (not only font size/weight).123124**Compound spacing:** Derive spacing from Figma **per variant cell**, not from a single axis (e.g. “if boxed, always gap-2”). Size and shape often change gap/padding independently — document or test non-default cells when logic is non-obvious.125126**Spacing verification rules**127128- Record **pl and pr separately** in the spacing table — never assume symmetric padding.129- Do **not** infer padding from symbol bounding-box width or total component width.130- Verify padding on the inner **State-Overlays** frame (Dev Mode or MCP codegen on the variant cell).131- When `Spacing/N` tokens appear together (e.g. `Spacing/8` + `Spacing/12`), map each to its side; do not dismiss larger tokens as "internal only" without checking the overlay frame.132- **Shared `cva` ≠ shared spacing** — if a sibling component (e.g. Tag vs TagToggle) diverges, document and fix per component file.133134**Interactive states** (on controls inside the component):135136| State | Verify |137| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |138| Hover / pressed | overlay tokens (+ `-inverse` where spec uses inverse surfaces) |139| Focus | ring token, width, offset; fill/icons usually unchanged vs enabled — pull the **focused** Figma cell |140| Dropdown-open / expanded | **Separate Figma cell** from focus. Never copy `focus-visible` styles onto `data-[state=open]` without comparing both cells. Re-check nested IconShell Type + opacity when the parent surface flips (often dark/`active-inverse` + light/`neutral` icons). **Red flag:** shared `focus-visible:… data-[state=open]:…` classes with no focus-vs-open Figma comparison. |141| Toggle-on / selected | `data-[state=*]` / `aria-*` branches; icon tone if surface flips |142| Disabled | muted fill, disabled text, overlay |143| Loading | if defined in Figma |144| Error (field) | control border/fill (`border-stroke-status-*` / `bg-status-*`) **and** feedback text (`text-status-*`, per-size typography) |145146Composition components (split buttons, icon+menu triggers): verify open on the **trigger segment** that receives `data-state=open`, including nested IconShell — not only the parent `cva` file.147148**Visual pass (required):** `npm run dev` on the demo vs Figma / `get_screenshot` per matrix cell. Fix or document any **≥2px** mismatch.149150Report a **variant × state** matrix: pass / drift (note ≥2px or wrong token).151152**Figma MCP (when available):** `get_design_context` (primary — layout, description, screenshot), `get_metadata`, `get_variable_defs`, `get_screenshot`.153154## Acceptance checklist155156- [ ] Alignment table: all Figma axes ↔ `cva`/props (both directions); SLOT seams covered157- [ ] Every Figma `state` value has a matrix cell; when both **focused** and **dropdown-open** exist, open ≠ focus verified (fill + nested IconShell type/opacity)158- [ ] Code Connect (if present): enums, size names, variant→prop mapping verified against Figma + React API (not the reverse); drift fixed — including open/expanded not collapsed to enabled/`false` when Figma differs159- [ ] Field chrome table (when Elements/\* present): label, helper, feedback, counter — typography + color per **size**160- [ ] Feedback / status copy uses theme utilities `text-status-error|warning|success|information` (Figma `Text/*` → `text-status-*` is OK)161- [ ] Variant × state matrix: tokens + geometry per cell162- [ ] CTA `button-*` vs `button-link-*` (underline) checked per variant × state — including ghost/link-like types at **enabled**163- [ ] Light and dark where the component appears on both164- [ ] Defaults aligned (Figma, `cva`, registry); demos start simple and cover the full alignment table165- [ ] Compound spacing from per-cell Figma values (no undocumented single-axis shortcuts)166- [ ] Horizontal padding verified as pl + pr per matrix cell (not width-inferred, not assumed symmetric)167- [ ] Shared styling helpers checked per component when spacing differs168- [ ] Exported sub-components: demo + test, or not exported169- [ ] Composed primitives: correct `asChild` direction; keyboard nav verified170- [ ] Visual pass: no undocumented ≥2px gaps171- [ ] Exit gate passes — see [create-qbds-component](../create-qbds-component/SKILL.md#exit-gate)172173## User only shares a URL174175Extract `node-id` from the URL, run the workflow above, and compare to the nearest sibling in `src/components/ui/`.
Run npx skillmds@latest add mckinsey/figma-parity in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Match Figma specs when implementing or reviewing QBDS UI — tokens, layout, all variants/states, Code Connect, demos, registry. Use when implementing, updating, or reviewing a QBDS component against a Figma URL/node or the library spec. Triggers — "implement this Figma component", "match the Figma spec", "figma parity", "build this component from Figma", or a figma.com URL alongside work in src/components/ui/, demos, registry.json, or code-connect/. It is listed under Design & Media on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
mckinsey (@mckinsey) published this skill. Their other Agent Skills are listed on their SkillMD profile.