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
.envhas the full URL → runnpm run figma:config - If
.envvalue is empty → ask user to paste the Figma URL into.env, then runnpm 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:
// 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 writesdocumentUrlSubstitutionsintofigma.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 variant38573-15380).
2 — Files, imports
- Mappings live in
code-connect/<name>.figma.ts(already globbed byfigma.config.template.jsoninclude). - 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 andid.
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(noshapeprop) →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:
- Helper:
getBoolean(...)andfindInstanceof the help/hint layer withtype === 'INSTANCE' - Feedback:
getBoolean(...)andfindInstanceof the status/feedback layer withtype === 'INSTANCE' - Copy from
getStringon that instance (JSON.stringify→{${lit}}). Demo fallback only when the instance exists but the string is empty - 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.
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 relatedstateenum value exists. - Prefer
executeTemplate()on nested instances that already have Code Connect. Hand-roll a minimal sibling snippet only whenhasCodeConnect()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.
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):
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 whenconnectedInstancesis 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');
// 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 propsradio-group-list-vertical.figma.ts— enum??fallback,reg: 'default', SLOT viagetSlot().connectedInstancessonner.figma.ts—JSON.stringifyfor instance stringsdialog.figma.ts— optional region toggles, conditional wrapper parts,renderChildrencard.figma.ts— omit default prop values in generated snippetbutton-group.figma.ts,tag-group-dismissable.figma.ts— olderproperties.childrenpatterntextarea.figma.ts— helper/feedback strings fromfindInstance+getString(boolean + layer)
Validate & publish
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)