Axon 911 Frontend Docs
    Loading navigation…
U
User
Switch to light mode
  1. Overview
  2. Figma Code Connect

Figma Code Connect

How Figma Dev Mode shows real @prepared911/ui snippets, where the templates live, and the check that keeps them honest.

Code Connect replaces the generic Dev Mode snippet for a component instance with one that uses the real @prepared911/ui component and props. The reference file is the Axon 911 Design System in Figma. A Figma component set maps to a code component only when its properties mirror the code props: same names, same values, same defaults.
Where the templates live
Each template sits beside the component it maps and is owned by the same team.
UnitTemplate
Componentpackages/ui/src/components/<id>/<node-name>.figma.ts
Blockpackages/ui/src/blocks/<id>/<node-name>.figma.ts
There is one template per Figma component set or standalone component, named for the node in kebab-case (card.figma.ts, card-footer.figma.ts, menu-sub-trigger.figma.ts). A unit with several nodes has several templates, and each template's component header names the export that node stands for. Template ids (card-footer) are unique across the package. A template is a parserless figma.code template. It starts with three header lines and ends with a default export that has an id:
Map every Figma property that has a code prop. Do not write a prop the component does not declare; if a Figma property has no code prop, record it as a parity mismatch for the Figma owner, or classify it with the conventions below. Never invent a mapping to look complete: a gap that is written down is the product of the work.
Header lists
Optional // lines after component= say what the snippet cannot express. All are comma-separated names, and the verifier checks them:
HeaderLists
// no-code-prop=Figma properties on the set that have no code prop to map to. A parity gap for the Figma owner.
// no-figma-property=Props of the component that a Figma control could stand for (an enum or union of literals, a boolean, a slot, visible copy) with no property on the set. Every such prop is either written by the snippet or listed here.
// story-only=Axes drawn only to show a look the browser makes.
// retired=Properties still on the set that another property replaced.
// note=Free text about a gap or a choice; may repeat.
A template that lists a no-code-prop name, or a no-figma-property name the component declares as a design prop, is partial. A template with neither is mapped. pnpm --filter @prepared911/ui code-connect:coverage prints both counts and every gap. A template never reads a property and discards the value, never imports a named glyph, and never writes className or style. A swapped icon for a ReactNode prop goes through getInstanceSwap(...).executeTemplate(); a prop that takes a glyph reference gets a lower-case placeholder (icon={glyph}) and the swap is listed under no-code-prop, because Code Connect cannot name the swapped glyph.
The check
pnpm --filter @prepared911/ui code-connect:check runs in ci:gates and check. It fails when:
  • a published component or block has no template, is not exempt in scripts/code-connect/no-figma-counterpart.json and is not in scripts/code-connect/unmapped-baseline.json;
  • a template writes a prop that the catalog does not list for the component;
  • a template names an enum member the code does not export;
  • a template's source or component does not resolve, a header line is missing, or the url is outside the Design System file;
  • a baseline id now has a template, or is no longer published;
  • an exemption has no reason, is listed twice, is also in the baseline, has a template, or is no longer published.
  • a unit that has a template exports a component with no template of its own that is not listed, with a reason, in scripts/code-connect/parts-without-figma-set.json;
  • a part exemption has no reason, is listed twice, names something that is not an exported component, or has a template.
A new component ships with a template, or the pull request adds its id to the baseline and says why. The baseline only shrinks, and it is empty today. A non-visual part, one with no box, paint or semantics of its own (Contents, Portal, VisuallyHidden), has no Figma counterpart to map. It goes in scripts/code-connect/no-figma-counterpart.json, one id per entry with a written reason, and never in the baseline.
Conventions for Figma axes
One rule per kind of axis, so every template reads the same way. Read each Figma property once, at the top of the template, with the getter that matches its Figma type.
Figma propertyTemplate
BOOLEANgetBoolean("Loading")
VARIANT whose options are False and True (boolean as a variant: Disabled, Icon only)getEnum("Disabled", { False: false, True: true }). getBoolean is for BOOLEAN properties only; the drift check flags it on a variant. The axis is named for the code prop in sentence case. When there is no such prop, the axis picks a branch of a union (Icon only picks icon plus label instead of children) and the template says so in a constant.
VARIANT with a third option (Toggle: None, Off, On)getEnum to lowercase words ("none", "off", "on"), then derive the props from the words.
VARIANT that is an enum in code (Intent, Variant, Size)getEnum with one entry per Figma option, mapped to the enum member ("ButtonIntent.Accent").
TEXT, SLOTgetString, getSlot. Copy is a copy("...") placeholder, never the Figma text (ADR 0009).
Story-only axes (State, Scene, Story) exist so Figma can draw a look the browser makes by itself. Never invent a prop for one. Apply the first rule that fits:
  1. The component has a prop that forces the look: emit it, and only when no other prop conflicts. Button State=Pressed emits active.
  2. The look is a pseudo-class and no prop forces it: emit one // line at the top of the snippet that names the pseudo-class, and no prop. Button State=Hover and State=Focus do this.
  3. The axis is only a scene or a story (Scene): do not read it, and list it in the header as // story-only=Scene.
A property that stays on the Figma set after another one replaced it goes in the header as // retired=Show label,Pressed. The drift check then stops asking for a mapping and reminds you to remove it from Figma. Anything that a snippet cannot express is written down in the component's table below.
Button
The Button set (node 2072:3) has these properties: Intent, Variant, Size, Disabled, State, Icon only, Toggle, Loading, Show leading, Show trailing, Label, Leading and Trailing. Show label and Pressed are retired: Icon only and Toggle replace them.
FigmaSnippet
Icon only=Trueicon={<Leading />} and label={copy(...)}, no children, no leading or trailing
Icon only=Falsechildren, plus leading and trailing when Show leading and Show trailing are on
Toggle=Onpressed and onPressedChange={handlePressedChange}
Toggle=Offpressed={false} and onPressedChange={handlePressedChange}
Toggle=Noneno toggle props
State=Restnothing
State=Pressedactive (data-pressed, no aria-pressed), except on a toggle, where active is not defined
State=Hovera comment: the browser paints :hover, Button has no prop that forces it
State=Focusa comment: the browser paints :focus-visible, Button has no prop that forces it
Disabled=Truedisabled
Loading onloading and loadingLabel={copy(...)}, except on a toggle, where loading is not defined
What a snippet cannot say: tooltip, type, pressedIcon, defaultPressed, preventFocusOnPress, className and native attributes have no Figma property. Hover and Focus cannot be set in code at all. handlePressedChange and the copy keys are placeholders the developer fills in.
The verifier
code-connect:check reads only the repository. code-connect:verify also reads scripts/code-connect/figma-sets.snapshot.json, a checked-in read of every component set and standalone component in the Design System file (property names, types, defaults and options, without the icon lists). It runs in the unit tests (tests/code-connect-templates.test.ts):
For each template it finds the node in the url, renders the snippet for every combination of the set's variant and boolean properties (a seeded sample when the product is above 600), and fails when:
  • a getX reads a property the set does not have, with the wrong getter, or a getEnum map skips or invents an option;
  • a live property is neither read nor listed in a header list, or a header list names something that is not on the set;
  • a snippet is empty, writes className or style, writes a prop its component does not declare, or names an enum member that does not exist;
  • a design prop is neither written nor listed under no-figma-property;
  • with --types, a rendered snippet does not type-check against the real components. copy, lower-case placeholders and <Name>Icon stand-ins are the only names a snippet may use without importing them.
When the designers change a set, refresh the read, then fix the templates it breaks:
The snapshot is the offline copy of the file. The nightly drift check below is the one that reads Figma itself.
The drift check
code-connect:check reads only the repository, so it cannot see Figma change under a template. code-connect:drift closes that gap:
For every template it reads the live component set (one request per Figma file) and runs the template against the set's real property definitions. It reports:
  • a property the template reads that the set no longer has, or reads with the wrong getter (getBoolean on a variant);
  • a getEnum map that skips an option the set gained, or names one it lost;
  • a Figma property that the template neither reads nor lists under // story-only=, // retired= or // no-code-prop=.
It exits 1 on any of these. A retired property that Figma still declares is only a note. The code-connect-drift workflow runs it every night and on demand with a Figma token read from the FIGMA_ACCESS_TOKEN repository secret, or the Figma repository secret when that one is empty, and a failed run is the alert. When neither secret is set the run skips with a warning that names both. It is not a pull request check and not a required check.
Try a template
This parses every template and prints what would be published. It needs no Figma token. publish --dry-run does need one: it lists the files it would publish, then exits 1 with "Couldn't find a Figma access token". Publishing is a separate, manual step; see the Figma Code Connect decision record in packages/ui/docs/decisions/0030-figma-code-connect.md.
Coverage
pnpm --filter @prepared911/ui code-connect:coverage prints, for every exported component, whether it is mapped, partial (a template that lists gaps), without a Figma counterpart (exempt with a reason) or unmapped, and lists the gaps of each partial one. A part that Figma draws inside its parent's set (CardContent as the Card Content slot) is exempt in parts-without-figma-set.json; a whole unit with no Figma component (value-list-field) is exempt in no-figma-counterpart.json.
Previous
Testing
Next
Releases
On this page
Where the templates live
Header lists
The check
Conventions for Figma axes
Button
The verifier
The drift check
Try a template
Coverage