Custom Components
Custom mapping (mapFn)

Custom mapping (mapFn)

Use prop.config.mapFn in locofy.config.json when declarative mapping is not enough: filtering nodes by type or text, building values from several layers, turning a group of TEXT layers into an array of objects, or any small bit of logic over the component instance tree in Figma.

This page is the reference for how mapFn runs and which helpers exist. For the rest of custom components (plugin UI, figmaPropName, config.layer, examples), start with Manual Prop Mapping.

Note: If `mapFn` is set for a prop, it is the only detection strategy for that prop—there is no fallback to layer naming or `config.layer`. See Detection order below.

Detection order (per prop)

PriorityStrategy
0prop.config.mapFn — if present, only this runs. There is no fallback to naming convention, config.layer, or structural mapping. Errors, null, and undefined still resolve to the default empty value for that prop; other strategies are not retried.
1Layer naming conventions ([propName], [propName >], predefined pairs)
2User config (config.layer, figmaPropName, Figma component properties)
3Node-id structural mapping

How mapFn works

Declarative strategies answer which layer holds this value? mapFn answers how do I compute this prop from the tree?—for example filter by type or text, derive an href from label text, or collect siblings into an array.

Put a JavaScript expression (not a statement block) in prop.config.mapFn. It runs at code-generation time inside the Figma plugin (in your browser). The expression must return the final prop value matching the prop’s type (string, number, boolean, object, array, etc.).

API available inside mapFn

Only the bindings below are in scope (no imports, no closure variables from your app):

NameDescription
query(selector)First node matching a name or id path under the instance root (segments joined with >).
queryAll(selector)All nodes matching the path’s last segment under the resolved subtree.
queryVisible / queryAllVisibleSame as query / queryAll but omit hidden nodes.
rootNodeProxy for the instance root.
find / findAllPredicate search over the subtree (depth-first).
image(node)Image export sentinel for a node (pending_<nodeId> → resolved at build time).
component(node)Custom-component reference sentinel (resolved in generated code).
clean(value)Recursively remove null / undefined from objects and arrays.

These helpers are also available as methods on any NodeProxy (e.g. query('Nav').queryAll('Label'), n.queryVisible('Title')?.text).

query selector syntax

Segments are joined with >. Each segment is either a numeric node id (3434:343) or a layer name (exact match). Resolution walks from the previous match (or instance root). Examples: query('Link group'), query('Main > Nav > Label'), query('Main > 3434:343 > Label').

NodeProxy

Wraps Figma node data with visible (instance visibility), children (array of proxies), query / queryAll / find / findAll, and passes through raw fields such as id, name, type, text, layoutMode, fills, etc.

query vs find

query / queryAll follow breadth-first segment paths when you know layer names. find / findAll use depth-first predicates when you need type, visibility, or custom conditions.

Errors and limitations

Errors — Runtime errors are caught and logged; the prop falls back to the default value. Prefer optional chaining: query('Optional')?.text ?? ''.

Limitations — Synchronous only (no async / direct Figma API calls from the expression). Scoped to the current component instance (no cross-canvas lookup). text is a plain string (no rich-text runs). Treat mapFn strings as trusted code from your repo; do not load them from untrusted sources.

Examples

// String from a named layer
query('Button Label').text;
// Array of { name, href } from TEXT children
query('nav links')
  .children.filter((t) => t.type === 'TEXT' && t.visible && t.text !== '.')
  .map((t) => ({
    name: t.text,
    href: '/' + t.text.replaceAll(' ', '-').toLowerCase(),
  }));
// Optional layer
query('Subtitle')?.text ?? '';
// clean() with queryVisible
queryAllVisible('Meta item').map((n) =>
  clean({
    title: n.queryVisible('Title')?.text,
    value: n.queryVisible('Value')?.text,
    icon: component(n.queryVisible('Icon')),
  }),
);

Angular: named slot for a node prop

For Angular components that use named slots (e.g. slot="icon" on projected content), you can combine attr: "slot", config.nodeKind: "slot", and mapFn to pick which Figma child maps to that slot—typically the first swapped instance under the card root.

Note: The nodeKind slot option and this pattern are for Angular (content projection / ng-content). See Manual Prop Mapping for the full nodeKind table.

locofy.config.json (prop on your card component)

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

mapFn returns the first direct child when it is an INSTANCE (your icon component in Figma); otherwise undefined. Adjust the expression if your hierarchy uses a wrapper frame—e.g. root.query('Icon slot') or a different child index.

Generated template (conceptual)

The icon child is emitted on the icon slot so it matches your component API:

<div class="demo-cell">
  <p class="demo-label">Grey Background</p>
  <my-icon-card
    size="large"
    cardStyle="grey-bg"
    title="Scheduled reports and deadlines"
  >
    <my-icon-calendar slot="icon"></my-icon-calendar>
  </my-icon-card>
</div>

Here slot="icon" lines up with the prop named icon and attr: "slot" in config.

Config snippet

Array props derived from layers often look like this (see Manual Prop Mapping — Example 3):

{
  "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() }))"
  }
}