Figma's node tree is rich but noisy. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — each has dozens of optional fields, some of which conflict with each other. An agent working directly off the raw Figma API response spends cognitive budget understanding the schema instead of writing code.

figmascope normalizes the tree into four node kinds: stack, overlay, absolute, and leaf. Every node in every screens/*.json file is one of these four. Nothing else.

The four kinds

stack

A stack is a node with Figma Auto Layout — layoutMode: "VERTICAL" or layoutMode: "HORIZONTAL". Its children are flow-positioned along the axis. Gaps and padding are explicit.

{
  "kind": "stack",
  "id": "123:456",
  "name": "ContentColumn",
  "axis": "vertical",
  "gap": 16,
  "paddingTop": 24,
  "paddingBottom": 24,
  "paddingLeft": 20,
  "paddingRight": 20,
  "primaryAxisAlignItems": "SPACE_BETWEEN",
  "counterAxisAlignItems": "CENTER",
  "width": 390,
  "height": 844,
  "fills": [{ "type": "SOLID", "color": "#1a1a2e" }],
  "children": [ ... ]
}

Maps to Jetpack Compose as a Column (vertical axis) or Row (horizontal axis). The primaryAxisAlignItems and counterAxisAlignItems fields map to Arrangement and Alignment respectively. gap becomes Arrangement.spacedBy().

overlay

An overlay is a node whose children have absolute positions within it. Figma represents this as a Frame with layoutMode: "NONE" where children have absoluteBoundingBox coordinates. The overlay kind captures this without requiring the agent to reason about raw coordinates to understand the structure.

{
  "kind": "overlay",
  "id": "123:789",
  "name": "CardOverlay",
  "width": 358,
  "height": 200,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "children": [
    {
      "kind": "absolute",
      "offset": { "x": 16, "y": 16 },
      "child": { ... }
    }
  ]
}

Maps to Compose as a Box. Children are positioned using Modifier.offset() or Modifier.align() depending on how their positions relate to the parent bounds.

absolute

An absolute node is a wrapper that carries a single child at a specific (x, y) offset within its parent overlay. It's a thin structural node — it exists to preserve the spatial relationship from Figma without conflating position with content.

{
  "kind": "absolute",
  "offset": { "x": 24, "y": 140 },
  "child": {
    "kind": "leaf",
    "name": "BadgeLabel",
    "text": "NEW",
    "stringRef": "badge.new",
    ...
  }
}

In Compose, this typically becomes a child of a Box with Modifier.offset(x.dp, y.dp). The offset values are candidates for token substitution if spacing tokens exist in range.

leaf

A leaf is a terminal node — no children. It has styling (fills, strokes, corner radius) and optionally text content. Text leaves have a text field (the literal string) and a stringRef field (the i18n resource key from strings.json).

{
  "kind": "leaf",
  "id": "123:101",
  "name": "SpeedLabel",
  "text": "Speed Test",
  "stringRef": "speed.test",
  "fontSize": 18,
  "fontWeight": 600,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "width": 160,
  "height": 28
}

Maps to Compose as a Text() composable for text content, or as a Surface, Box, or Image for non-text leaves depending on fill type.

The four kinds map one-to-one to the compositional model of any UI framework. Stacks are flow layouts. Overlays are z-positioned containers. Absolutes carry spatial metadata. Leaves are content. That's it. The Figma node taxonomy has 12+ node types — the IR reduces it to four.

How kind is determined

The classification logic:

  1. If layoutMode is "VERTICAL" or "HORIZONTAL"stack
  2. If layoutMode is "NONE" and node has children → overlay (children wrapped as absolute)
  3. If a node is the direct child of an overlay and carries a position → absolute
  4. If node has no children and is not an absolute wrapper → leaf

The layout-mode-none-inferred warning in _meta.json fires when a Frame with layoutMode: "NONE" has children with non-trivially overlapping bounding boxes. figmascope treats it as an overlay, but notes the inference because some layoutMode: "NONE" frames in practice are just containers for a single child (zero overlap) and could be simplified. The warning lets the agent decide how to handle it.

absoluteBoundingBox — why it's preserved

Every node in the IR retains its absoluteBoundingBox from Figma, even when the parent is a stack (where absolute positions are theoretically irrelevant to layout):

{
  "kind": "stack",
  "absoluteBoundingBox": { "x": 0, "y": 88, "width": 390, "height": 756 },
  ...
}

This is there for agents that need to reason about spatial relationships across nodes that don't share a direct parent-child relationship. Overlapping two elements from different branches of the tree requires knowing their absolute positions, not just their relative offsets. The coordinate system is Figma's canvas — top-left origin, y increasing downward.

It also helps with PNG cross-reference. The _meta.json includes a pngCount field and the exported PNGs are named by screen slug. If you're comparing the IR to the screenshot, absoluteBoundingBox lets you locate specific nodes within the image. To see this on your own design, export a bundle from figmascope.dev.

Fills and strokes on container kinds

A common point of confusion: stack and overlay nodes can have fills, not just leaves. A column with a background color is still a stack — it just also has a fills array. figmascope preserves this:

{
  "kind": "stack",
  "axis": "vertical",
  "fills": [{ "type": "SOLID", "color": "#f6f2ea" }],
  "cornerRadius": 12,
  ...
}

In Compose, this typically becomes a Column inside a Surface or Card, with the fill color and corner radius applied to the container. The IR doesn't collapse the fill into the children — it keeps it on the node where Figma set it.

Gradient fills on container kinds emit a background-gradient-not-supported:<name> warning. The node is still present in the IR with its other fields intact. The agent should treat the fill as a TODO and either approximate with a solid color or generate a custom drawing call, per the CONTEXT.md scope notes.

The stringRef + text relationship

Text leaf nodes carry both:

If a text node's content was dropped from strings.json due to a collision or filter (numeric-only, empty, too short), the leaf still has text but stringRef will be absent. The agent should fall back to the literal in that case. See strings.json for the full collision handling logic.

When both are present, the CONTEXT.md constraint says to use stringRef. The text field is there for agent reasoning (so it knows what the string says without opening strings.json) and as a fallback.

Component instances in the IR

When a Figma node is an INSTANCE of a component, the IR node carries two additional fields:

{
  "kind": "stack",
  "componentId": "789:012",
  "componentName": "PrimaryButton",
  ...
}

This links back to components/inventory.json. The agent knows this node is an instance of PrimaryButton rather than a bespoke layout, and should reference the existing component rather than generating duplicate code. The full coverage of this is in Component Inventory.

Real screen example

A simplified but structurally accurate example of a screen with a header, content column, and floating button:

{
  "screen": "home",
  "root": {
    "kind": "overlay",
    "name": "HomeScreen",
    "width": 390,
    "height": 844,
    "fills": [{ "type": "SOLID", "color": "#0d0d1a" }],
    "children": [
      {
        "kind": "absolute",
        "offset": { "x": 0, "y": 0 },
        "child": {
          "kind": "stack",
          "name": "ContentArea",
          "axis": "vertical",
          "gap": 24,
          "paddingTop": 56,
          "paddingLeft": 20,
          "paddingRight": 20,
          "children": [
            {
              "kind": "leaf",
              "name": "Title",
              "text": "Speed Test",
              "stringRef": "speed.test",
              "fontSize": 28,
              "fontWeight": 700
            }
          ]
        }
      },
      {
        "kind": "absolute",
        "offset": { "x": 111, "y": 752 },
        "child": {
          "kind": "stack",
          "name": "StartButton",
          "componentId": "321:654",
          "componentName": "PrimaryButton",
          "axis": "horizontal",
          "gap": 8,
          "paddingTop": 16,
          "paddingBottom": 16,
          "paddingLeft": 32,
          "paddingRight": 32,
          "cornerRadius": 24,
          "fills": [{ "type": "SOLID", "color": "#7f5cfe" }]
        }
      }
    ]
  }
}

From this the agent can correctly generate a Compose Box screen with an absolute-positioned Column for content and an absolute-positioned PrimaryButton component at the bottom. Every layout decision is derivable from the IR without guessing.

For how tokens apply to the spacing and color values in this structure, see tokens.json Explained. For the full agent workflow using this IR with Cursor or Claude Code, see Jetpack Compose from Figma. Try it yourself by pasting your Figma URL into figmascope.