Design System
Complex prop mapping (mapFn)

Complex prop mapping with mapFn

The problem: Normal Locofy mapping answers which layer has this value?—using names, config.layer, or Figma component properties. That breaks down when you need to build a value from the tree: filter by layer type, turn several TEXT layers into an array, combine fields, read component properties from the instance, or apply any small rule that depends on how layers relate.

The solution: Put a JavaScript expression (a single string) in prop.config.mapFn for that prop. When code is generated, Locofy runs it against the layers inside that component instance and uses the result as the prop value. For plugin flows, figmaPropName, and config.layer, see Manual Prop Mapping.


Example: links from a header (Figma → code → config)

In Figma

Your Header instance has a frame nav links. Inside it, each nav item is a TEXT layer (e.g. Home, Listings, Account). Figma has no “array” component property for that list—you only have layers.

Header (instance)
└── nav links
    ├── Home        (TEXT)
    ├── Listings    (TEXT)
    └── Account     (TEXT)

In your component

You want a prop typed as an array of { name, href }:

links: { name: string; href: string }[];

In locofy.config.json

Set the prop to type: "array" and add config.mapFn with an expression that finds nav links, keeps visible TEXT nodes, and maps them to objects:

{
  "name": "links",
  "type": "array",
  "required": true,
  "previewValue": [{ "name": "Home", "href": "/" }],
  "config": {
    "mapFn": "query('nav links').children.filter(t => t.type === 'TEXT' && t.visible).map(t => ({ name: t.text, href: '/' + t.text.replaceAll(' ', '-').toLowerCase() }))"
  }
}

query('nav links') finds that frame under the instance; .children are the TEXT layers; the .map(...) builds each { name, href }. Adjust names and href logic to match your design.

More examples like this live under Example 3 — Header links in the manual.

Note: If `mapFn` is set for a prop, **only** that expression is used for that prop—there is no fallback to layer naming or `config.layer`. Fix errors in the expression or the prop gets its default empty value.

Detection order (reminder)

PriorityStrategy
0config.mapFn — runs alone for that prop
1–3Naming, user config, structural mapping — used only when mapFn is not set

Writing the expression

  • Put one expression in the string (no return block). Only the helpers below exist—no imports, fetch, or variables from your app.
  • Return a value that matches the prop’s type in locofy.config.json (string, number, boolean, object, array, …).
  • Prefer layer names in paths like 'Card > Title'. Use optional chaining if a layer might be missing: query('Subtitle')?.text ?? ''.

Helpers

HelperWhat it does
query('A > B')First match for a path of layer names (> between segments). Shallowest match wins at each step.
queryAll, queryVisible, queryAllVisibleAll matches / skip hidden / same on NodeProxy as methods.
rootThe instance root.
find / findAllFind nodes with a function (e.g. n => n.type === 'TEXT') when a fixed path is not enough.
image(node) / bgImage(node)Export a layer as an image asset; bgImage is for CSS background.
component(node)Reference another mapped custom component in generated code.
clean(obj)Remove null / undefined from nested objects and arrays (handy with optional fields).

query vs find: query follows layer name paths. find uses a predicate—useful for type, visibility, or custom rules.

NodeProxy: Nodes expose children, visible, text, name, type, properties (Figma component properties for that instance), etc. On the root, root.properties?.['Show icon'] reads sidebar properties by label.

clean() example

queryAllVisible('Meta row').map((n) =>
  clean({
    title: n.queryVisible('Title')?.text,
    value: n.queryVisible('Value')?.text,
    icon: component(n.queryVisible('Icon')),
  }),
);

Quick patterns

GoalSketch
String from a layerquery('Button Label').text
Longer path / disambiguatequery('Card > Header > Title').text
Boolean from visibilityquery('Icon').visible
Figma component propertyroot.properties?.['Variant'] ?? 'primary'
Array from framesquery('Nav').children.filter(...).map(...)
Optional layerquery('Subtitle')?.text ?? ''

Errors throw → logged → prop gets default; no fallback to other mapping strategies.

Limits: synchronous only; read-only; this instance only; text is plain string; keep expressions in your config only.


Angular: named slot for a node prop

For named slots (slot="icon") with ng-content, combine attr: "slot", config.nodeKind: "slot", and mapFn to choose which child maps to the slot—often the first instance child:

{
  "name": "icon",
  "attr": "slot",
  "type": "node",
  "config": {
    "nodeKind": "slot",
    "mapFn": "root.children[0]?.type === 'INSTANCE' ? root.children[0] : undefined"
  }
}

Adjust for wrapper frames (e.g. root.query('Icon slot')). See Manual Prop Mapping for nodeKind.

Note: This pattern is for Angular content projection. React/Vue do not use `config.nodeKind` the same way.

Conceptual output

<gps-icon-card ...>
  <gps-icon-calendar slot="icon"></gps-icon-calendar>
</gps-icon-card>