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.
Detection order (per prop)
| Priority | Strategy |
|---|---|
| 0 | prop.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. |
| 1 | Layer naming conventions ([propName], [propName >], predefined pairs) |
| 2 | User config (config.layer, figmaPropName, Figma component properties) |
| 3 | Node-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):
| Name | Description |
|---|---|
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 / queryAllVisible | Same as query / queryAll but omit hidden nodes. |
root | NodeProxy for the instance root. |
find / findAll | Predicate 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.
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() }))"
}
}