| name | code-connect |
| description | 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/. |
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:
- Still add the empty key to
.env.example (committed).
- Stop and give the user an exact block to paste into their local
.env:
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:
- 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 showHelpText and showFeedbackMessage as separate booleans. React composes one footer message:
| State | Render |
|---|
| valid / neutral | <FieldDescription> when showHelpText |
| error / invalid | <FieldError> when showFeedbackMessage — replaces helper, do not also render FieldDescription |
Demos follow this (see TextareaStates error example). Code Connect templates must match — not both in the same snippet.
${invalid && showFeedback
? figma.code`<FieldError>${statusMessage}</FieldError>`
: showHelpText
? figma.code`<FieldDescription className="${descClass}">${helperText}</FieldDescription>`
: figma.code``}
4 — Slot children (repeated same-type instances)
Prefer Figma’s official SLOT path when the component has a SLOT property (see Writing template files):
getSlot('propName').connectedInstances + executeTemplate() / renderChildren — SLOT with code-connected children (expand snippets inline). Prefer this for new templates.
- Bare
getSlot('propName') — only when you want the Dev Mode slot pill (freeform content), not expanded children.
figma.properties.children(['MainComponentName']) — fallback when connectedInstances is empty (known quirk for some QBDS sets). Still used by older templates (tag groups, button groups).
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.
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}}:
const label = JSON.stringify(instance.getString('label') ?? 'Default label');
<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.
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
Validate & publish
npm run figma:config
npm run figma:parse
npm run figma:publish