# Code Connect

> Create or update a QBDS Figma Code Connect mapping as a template file (`code-connect/<name>.figma.ts`, MCP `figma` API, published via the Figma CLI). Use when adding a new mapping, wiring a Figma node URL, or mapping Figma component properties to a React component's API — including Figma-only props (show* booleans, slots) with no matching React prop. Triggers — "add a code connect", "create a Figma mapping", "map this component to Figma", "code connect for <component>", or a figma.com node URL alongside work in code-connect/.

- Skill: `mckinsey/code-connect` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mckinsey/code-connect`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mckinsey/code-connect/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: mckinsey (https://skillmd.com/u/mckinsey)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mckinsey/code-connect

---


# QBDS Code Connect (template-based)

Use the **`/figma-code-connect`** skill to create the template mappings — it owns the mechanics (URL parsing, `get_context_for_code_connect`, the `instance.*` API, enum/interpolation/dynamic-children rules, validation). This skill only layers the **QBDS conventions** below; don't restate the generic mechanics.

QBDS authors **template** files (`code-connect/<name>.figma.ts`, MCP `figma` API). The old parser style (`figma.connect(...)` in `.figma.tsx`) is **deprecated** — do not author new `.figma.tsx` files.

## Env vars (before templates)

Every `// url=<QBDS_*>` token needs a matching env var. Derive the name from the header:

| Template header             | Env var                      |
| --------------------------- | ---------------------------- |
| `// url=<QBDS_TAG>`         | `FIGMA_URL_QBDS_TAG`         |
| `// url=<QBDS_BUTTON_TEXT>` | `FIGMA_URL_QBDS_BUTTON_TEXT` |

Rule: `<QBDS_X>` → `FIGMA_URL_QBDS_X`.

### Missing var — add to repo files

When creating or wiring a template, check both files:

| File           | Action                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------- |
| `.env.example` | add empty placeholder if key missing: `FIGMA_URL_QBDS_<NAME>=`                                          |
| `.env`         | add key if missing — paste URL when user provided it in chat; otherwise empty: `FIGMA_URL_QBDS_<NAME>=` |

Then tell the user:

- If `.env` has the full URL → run `npm run figma:config`
- If `.env` value is empty → ask user to paste the Figma URL into `.env`, then run `npm run figma:config`

Never commit real URLs or tokens — only empty placeholders in `.env.example`.

### Cannot write `.env`

`.env` is local and may be missing or blocked. If you cannot create or edit it:

1. Still add the empty key to `.env.example` (committed).
2. Stop and give the user an exact block to paste into their local `.env`:

```bash
FIGMA_URL_QBDS_<NAME>=<full figma component-set url>
```

Do not run `npm run figma:config` / `figma:parse` until the user confirms the URL is in `.env` (empty values are skipped by `generate-figma-config.ts`).

## QBDS conventions

### 1 — Figma URL is a token, never inlined

The `// url=` header references a substitution token, not a raw URL:

```ts
// url=<QBDS_BUTTON_TEXT>
// source=src/components/ui/button.tsx
// component=Button
```

- Add the node URL via env vars (see **Env vars** above) — never inline the URL in the template.
- The script turns each `FIGMA_URL_<NAME>` into `<<NAME>>` and writes `documentUrlSubstitutions` into `figma.config.json` (git-ignored). The CLI substitutes `<QBDS_<NAME>>` → URL at publish.
- URL must target the **COMPONENT_SET** node, not a variant inside it. Dev Mode only surfaces Code Connect at the set level. Copy link from the set name in Figma (e.g. `Tags-Dismissable` → `38573-15379`, not variant `38573-15380`).

### 2 — Files, imports

- Mappings live in `code-connect/<name>.figma.ts` (already globbed by `figma.config.template.json` `include`).
- Import the React component from `@/components/ui/<name>`.
- One template = one Figma node. Distinct Figma variants that produce different snippets → **separate files** (e.g. `button-text.figma.ts`, `button-icon.figma.ts`), each with its own `<QBDS_*>` token and `id`.

### 3 — Show Figma-only props through the component

The component `Props` in `src/components/ui/<name>.tsx` is the source of truth. Some Figma properties have **no matching code prop** — don't drop them; render the same result through the component:

- `showLeadingIcon` (Figma-only boolean) → child instance inside the button.
- `shape: circle` (no `shape` prop) → `className="rounded-full"`.

If nothing represents it, omit it and tell the user. Keep `example` close to the demo (`src/app/demo/[name]/ui/<name>.tsx`).

**Map Figma prop types faithfully.** Use `figma.enum` / `getEnum` where the Figma property is an enum — do not collapse it to a boolean. Figma `on` and `state` are commonly enums (`state=enabled|hover|disabled`, not `false`); `getBoolean` is only for a genuine Figma boolean.

**Compose labels with the shipped `Label` API** — typography via `className`, gap by size. Do not invent a `size` prop on `Label` that the component does not have.

### 3b — Field footer: helper XOR feedback

Figma inputs often expose separate booleans for helper vs feedback (names vary: `hasHintText`, `hasHelpText`, `hasFeedbackMessage`, …). React composes **one** footer:

| State           | Render                                                              |
| --------------- | ------------------------------------------------------------------- |
| valid / neutral | `<FieldDescription>` when the helper toggle is on                   |
| error / invalid | `<FieldError>` when the feedback toggle is on — **replaces** helper |

Not both in the same snippet. Match demos (e.g. textarea error states).

**Do not invent footers.** Require **boolean + layer**:

1. Helper: `getBoolean(...)` **and** `findInstance` of the help/hint layer with `type === 'INSTANCE'`
2. Feedback: `getBoolean(...)` **and** `findInstance` of the status/feedback layer with `type === 'INSTANCE'`
3. Copy from `getString` on that instance (`JSON.stringify` → `{${lit}}`). Demo fallback only when the instance exists but the string is empty
4. Layer missing → omit footer. Do not emit placeholder helper/feedback from a boolean guess alone

Inspect real variants before wiring. Some sets keep feedback layers on the instance but hidden until a prop flips; if you cannot confirm the layer should appear for that variant, omit it.

```ts
const helpInst = instance.findInstance('/* help layer name from Figma */', {
  traverseInstances: true,
});
const statusInst = instance.findInstance('/* status layer name from Figma */', {
  traverseInstances: true,
});

const helperText =
  helpInst?.type === 'INSTANCE'
    ? JSON.stringify(helpInst.getString('helperText') || 'Helper text')
    : null;
const statusMessage =
  statusInst?.type === 'INSTANCE'
    ? JSON.stringify(
        statusInst.getString('statusMessage') || 'Feedback message',
      )
    : null;

const showErrorFooter = Boolean(invalid && showFeedback && statusMessage);
const showHintFooter = Boolean(
  !invalid && showHintText && helperText && !showErrorFooter,
);
```

### 3c — Compose only what Figma shows

- Emit optional regions (overlays, menus, popovers, footers, nested chrome) only when the corresponding **layer exists** on the selected instance (`findInstance` → `type === 'INSTANCE'`), not merely because a related `state` enum value exists.
- Prefer `executeTemplate()` on nested instances that already have Code Connect. Hand-roll a minimal sibling snippet only when `hasCodeConnect()` is false / mapping is missing.
- Do not hardcode demo values, selected state, or placeholder copy that is not on the Figma instance.
- Match demos for **composition shape**; gate each piece on Figma layers/props.

```ts
const overlayInst = instance.findInstance('/* overlay layer from Figma */', {
  traverseInstances: true,
});
const hasOverlay = overlayInst?.type === 'INSTANCE';
// wrap / include overlay snippet only when hasOverlay
```

### 4 — Slot children (repeated same-type instances)

Prefer Figma’s official SLOT path when the component has a SLOT property (see [Writing template files](https://developers.figma.com/docs/code-connect/template-files/)):

1. **`getSlot('propName').connectedInstances`** + `executeTemplate()` / `renderChildren` — SLOT with code-connected children (expand snippets inline). Prefer this for new templates.
2. **Bare `getSlot('propName')`** — only when you want the Dev Mode slot pill (freeform content), not expanded children.
3. **`figma.properties.children(['MainComponentName'])`** — fallback when `connectedInstances` is empty (known quirk for some QBDS sets). Still used by older templates (tag groups, button groups).

```ts
const slot = instance.getSlot('itemsSlot');
const connected = slot?.connectedInstances ?? [];
const items =
  connected.length > 0
    ? connected.map(n => n.executeTemplate().example).flat()
    : figma.properties.children(['RadioGroup/Item']);

export default {
  example: figma.code`
    <RadioGroup>
      ${figma.helpers.react.renderChildren(items)}
    </RadioGroup>
  `,
};
```

Do not call `executeTemplate()` on the slot itself — only on each `connectedInstances` handle. See `radio-group-list-vertical.figma.ts`, `radio-group-list-horizontal.figma.ts`.

Do **not** hand-roll nested snippets (e.g. inline `<Tag>` / `<NumericBadge>` inside Select) when those children already have Code Connect. Prefer `executeTemplate()` so the child’s mapping owns the snippet and imports. Only hand-roll when the child has no mapping, or `executeTemplate()` fails for that node.

### 5 — Template safety

**Enum fallback** — `getEnum` can return `undefined`. Always guard with `?? '<fallback>'` and type the result. Map Figma enum keys to React prop values faithfully — QBDS size sets often use `reg: 'default'` as the Figma key; that is intentional, not a typo.

```ts
const size = (instance.getEnum('size', {
  sm: 'sm',
  reg: 'default',
  lg: 'lg',
}) ?? 'default') as Size;
```

**Instance strings** — never interpolate raw `getString` values into JSX text. `JSON.stringify` the value and emit as a JSX expression `{${var}}`:

```ts
const label = JSON.stringify(instance.getString('label') || 'Default label');
// in figma.code:
<PartTitle>{${label}}</PartTitle>
```

**Slot children type** — connected slot results are always arrays. Interpolate with `figma.helpers.react.renderChildren()` — do not assign arrays and `figma.code`` to the same variable.

**Optional Figma-only regions** — when `getBoolean` toggles control optional UI (header slots, footer actions, etc.):

- Omit the entire part when the toggle is off — no placeholder elements
- Emit wrapper parts (footer, toolbar, etc.) only when at least one child toggle is on
- When only one side of a split layout is shown, use layout classes (e.g. `ml-auto`) on the visible side — do not insert empty nodes for alignment

**Default props in snippets** — omit props that match the component default.

**Fallback copy** — placeholder strings must match the demo exactly, including punctuation. Source: `src/app/demo/[name]/ui/<name>.tsx`. Never use fallback copy to invent UI that Figma does not show (see **3b** / **3c**).

## Reference examples

Read existing templates in `code-connect/` before writing a new one:

- `button-text.figma.ts`, `button-icon.figma.ts` — token header, variant split, Figma-only props
- `radio-group-list-vertical.figma.ts` — enum `??` fallback, `reg: 'default'`, SLOT via `getSlot().connectedInstances`
- `sonner.figma.ts` — `JSON.stringify` for instance strings
- `dialog.figma.ts` — optional region toggles, conditional wrapper parts, `renderChildren`
- `card.figma.ts` — omit default prop values in generated snippet
- `button-group.figma.ts`, `tag-group-dismissable.figma.ts` — older `properties.children` pattern
- `textarea.figma.ts` — helper/feedback strings from `findInstance` + `getString` (boolean + layer)

## Validate & publish

```bash
npm run figma:config        # regenerate substitutions
npm run figma:parse         # local template validation (exit 0)
npm run figma:publish       # only when the user asks (needs FIGMA_ACCESS_TOKEN)
```

