---
title: Figma
description: What the Figma importer carries into a Framewerk document, what it approximates, and what it does not carry.
status: shipped
url: https://getairframe.com/framewerk/integrations/figma
---

> Part of the Framewerk docs. Index of every page: /framewerk/llms.txt

# Figma

**Status: shipped.** The importer converts a Figma file into a Framewerk
document. This page is sourced from the converter and the import report —
what actually happens, not what might.

Airframe runs the import; the output is an ordinary Framewerk document you
can render, edit and export like any other.

## What it carries

### Layout and layers

| Source | Becomes |
| --- | --- |
| Frames with children | Screens (one per importable frame) with a layout tree |
| A top-level frame | Two nodes, for the two things it is in a prototype. The screen's root is a `screen-container`: the device's size, the one scroller. Its one child is the frame as drawn, under the frame's own `_id`, so a page taller than the device scrolls inside the screen as it does in Figma. The root is `<frame id>-screen`, adds no padding, and nothing inside the frame moves |
| Layers fixed to the screen (`FIXED_WHEN_CHILD_OF_SCROLLING_FRAME`) | A layer that spans the screen leaves the frame and stands in the screen's root, `position: "sticky"` at the height it was drawn, painted over the page and taking no room in it, so the page still starts under it |
| A frame drawn at a device's size | A screen on that device. A frame at a phone's width is the phone, never mobile web: Figma has no mobile-web frame, and the browser bar would paint over the design. The device most screens were drawn for becomes the project's default |
| A screen-sized frame at no device's size | A screen that keeps its own size |
| A status bar drawn into a phone mockup (`iOS/Status Bar`, 390×44 at the top) | Left out when the screen's device draws its own, so the two do not overlap; the report says how many. A bar that is a row of the frame's own stack stays |
| Layers in an auto-layout row or column | Every layer keeps its size — `flexShrink: 0` — as Figma never shrinks a layer; only a FILL layer gives way. A row too narrow for its fixed columns overflows, as it does in Figma |
| Auto-layout (gap, padding, direction, alignment) | `row` / `column` with `gap`, `padding`, `justify`, `align` |
| Frames that scroll (`scrollDirection`) | The frame scrolls along that axis inside the size Figma drew it at, with no scrollbar taking width: `overflowY: "auto"` for vertical, `overflowX: "auto"` for horizontal, `overflow: "auto"` for both |
| Layers pinned in a scrolling frame (`STICKY_SCROLLS`) | `position: "sticky"` at the frame's top edge, painted over what scrolls under it. In a free-form frame the layer is placed by margins, not by `top` and `left`, since a layer placed absolutely cannot stick: it draws where it was drawn, takes no room, and stops at the top when the frame scrolls. In a screen's own frame it stands in the screen's root, which is what scrolls |
| Solid single-colour vectors | `path` (or `svg` with `currentColor`) — themes with the style |
| Multi-colour / gradient vectors | A container with an inline SVG background; gradients become `<defs>` |
| Images | `image` with an asset ref |
| Shadows and blurs | `boxShadow` (or `filter: drop-shadow()` where the shadow follows the layer's shape), `backdropFilter` for a background blur, `filter` for a layer blur |
| Masks | Clipping to the mask's outline, wherever the mask stands in its parent: over it, inset, turned or mirrored. A rectangle over the whole parent is `overflow: "hidden"` with its `borderRadius`; any other is a `mask` holding the outline as a path in the parent's coordinates |
| Minimum and maximum sizes on a frame or a text (`minSize`, `maxSize`) | `minWidth`, `maxWidth`, `minHeight`, `maxHeight` |
| Page structure | Pages grouping imported screens |

### Type

| Source | Becomes |
| --- | --- |
| Text layers | `text` with `fontSize`, `fontWeight`, `fontFamily`, `lineHeight`, `letterSpacing`, case, decoration, alignment and colour |
| Line height set to Auto | The font's own line, as the multiple Figma recorded for that font (`lineHeight: 1.137` for GT Alpina, 1.21 for Inter), so the line stands at Figma's height even in a fallback face; `normal` when the file holds no record. A stated line height is carried as stated |
| A link from a range of text to a layer | `action: "scrollTo(<anchor>)"` on the run when the layer is on the same screen, `navigate(<screen>)` when the layer is a screen |
| Text with more than one style in it | Runs inside the `text`, each with its own style |
| A link on a range of text | `href` on that run, which opens the address |
| Text cut off with an ellipsis (`textTruncation`, `maxLines`) | One line: `whiteSpace: "nowrap"` with `textOverflow: "ellipsis"`. Several: a line clamp (`WebkitLineClamp`). A fixed box that states no line count gets the count Figma drew |
| Text styles | A rule in the imported style, `text[variant=<style name>]`, holding the style's size, weight, family, line height and letter spacing, and that `variant` on every layer whose values are the style's. Change the rule and every such layer follows |

### Colour, styles and variables

Everything here lands in one imported style, which the project is drawn in.

| Source | Becomes |
| --- | --- |
| Solid fill styles and colour variables | A token each, and `var(--<name>, <the colour>)` on every fill, stroke and text colour painted with one. A semantic variable that names a palette entry (`color-border/regular` → `Color/electric-surge-cyan/α30`) holds that entry's colour, whether it names it by key or by the variable's id |
| Colour variables with a light and a dark mode | One token with both values (`modes.light` / `modes.dark`), so the screen follows the mode it is rendered in. A layer painted with one keeps its literal colour and is not bound: Figma's mode for that layer is not read yet |
| Fill styles that are a gradient | A token that holds the gradient, and `background: "var(--<name>, <the gradient>)"` on every layer painted with it |
| Effect styles (named shadows) | A token that holds the shadow, and `boxShadow: "var(--<name>, <the shadow>)"` on every layer that casts it |
| Number variables | A token each: `--size-x05: 4px`, and an alias as a token that names another, `--size-spacing-s: var(--size-x05)` |
| A gap, corner radius or minimum height from a number variable | `var(--size-spacing-s, 4px)` in place of the number. The radius is bound when all four corners share the variable |
| A padding from a number variable | Four sides in place of the `padding` shorthand — `paddingTop` … `paddingLeft` — a token on each side that takes its value from a variable, the plain number on the others |

Every bound value keeps what it replaced as its fallback, so a layer draws
the same with the token as without it, and still draws if the token is
removed. A layer is bound only when its own value is the style's or the
variable's; one that differs keeps its own and is counted in the report.

### Components

| Source | Becomes |
| --- | --- |
| Components used more than once | One component definition and a `component` reference per instance. Where the instance sits and its size stay on the reference |
| Variant sets | A component named for the set, with a state per variant in use. It is drawn from the most-used variant; what each other variant changes is an `overrides` entry its state switches on, and each instance pins its variant with `state`. A variant that adds or removes layers folds in too: the component holds every variant's layers, each variant's in its own order, and a layer only some variants draw is hidden outside them by its `overrides` (`"hidden": true` for the states that do not draw it; a layer the most-used variant does not draw carries `"hidden": true` and is shown by the states that do). A variant that lacks a property the most-used variant has (no fill on a disabled input, no border on a selected step) folds in with that property set to `null` in its state's override, which removes it for that state. Two layers of one kind in one place are lined up by name; where they cannot be said as one layer with overrides and their names differ, both are kept, each drawn by its own variant. A layer that is a slot in one variant (its instances hold different components there) and a plain instance in another stays the slot in every state, and the other variant's instances pass what they hold, so an instance that swapped its icon keeps it when the pointer enters a hover state. A nested component that shows another of its own variants in one variant (a trend arrow falling in one banner, level in another) folds too: the state's override sets that nested reference's `state`. An instance that repaints or resizes what its state also sets (a disabled tag painted its own grey) keeps its own value: a state's override wins over an instance's, so that instance carries the layer's `overrides` list in its `nodeOverrides`, without its state's entry for that property |
| Text overridden on an instance | A string prop on the component, with each instance's text as its value |
| A boolean property of a component | A boolean prop on the component, named for the property in kebab case (`ShowIndicator` → `show-indicator`) at the component's default; the layer whose visibility reads it sits under `showWhen="{show-indicator}"`, and a reference passes the value only where it departs from the default. The layer's content is taken from an instance that shows it; what a hidden one holds is never drawn and is not imported. An instance that stays a copy is drawn as Figma drew it |
| An instance-swap property of a component | A slot on the component, named for the property in kebab case (`Icon` → `icon`), holding the property's default component as its own content; a reference whose instance assigns another component passes that component in the slot, and one that holds the default passes nothing. Where no instance in the file holds the default, the slot holds what the first instance does. The slot is `bare`, so what fills it stands exactly where the layer stood, and a layer a boolean property also shows and hides puts that condition on the slot. An instance that stays a copy is drawn as Figma drew it |
| A nested component that differs between instances | Where the instances of one component hold a nested component that is not the same in each (another component, other text, a recoloured layer, a variant), that place is a `bare` slot of the component, named for the layer. The component holds the content most instances have; an instance that departs passes its own nested component in the slot |
| Whether an instance keeps its size in a row or column | `flexShrink` on the reference, with its position and size, so one component serves an instance in a row and one placed by hand |
| Style overridden on an instance (colour, font size, font weight, alignment, spacing) | `nodeOverrides` on the reference, addressed by the layer's id in the component |
| An instance of a component from a shared library | The same, through the library's layer ids the copy carries (`overrideKey`); no library file is needed, since a `.fig` embeds every library component it uses |

### Prototype

| Source | Becomes |
| --- | --- |
| Reactions between frames (click, press, hover, timeout) | `flows[]` edges with triggers; a timeout edge keeps its wait as `delay`, in ms |
| A link after a delay (`AFTER_TIMEOUT`) | The screen moves on by itself: `onEnter: { after: <ms>, do: "navigate(<screen>)" }` on the screen whose frame carried it, or on the layer that did. Played in Play and the viewer; a screenshot or thumbnail shows the screen as it first draws |
| A click or press on a layer that goes to another frame (navigate, overlay, swap) | The layer gets the click: a `pressable` around it, with `action: "navigate(<screen>)"`. The flow edge between the two screens says how the screen arrives — pushed, presented over, swapped in |
| A hover link that stays on the screen (`ON_HOVER`, `MOUSE_ENTER`, `MOUSE_IN`, `WHILE_HOVERING`) — back, a web address, a scroll-to | `hoverAction` on the layer, run when the pointer enters it. `MOUSE_LEAVE` becomes `leaveAction`. A phone has no hover: on a screen designed for one, validate asks for a tap path too |
| A variant swap (`SWAP_STATE`) inside a set that imports as one component | A hover swap enters its target state while the pointer is over the instance (`interaction: "hover"`, `from` the state it leaves); a press, while it is down (`"pressed"`). A click changes the instance itself: the component declares `shown`, and the instances whose layer reacted pass `swapsOnClick`, which shows a click layer over the component that runs `setState(shown, <target>)`. A swap after a delay runs on a timer each time its state comes back (`swapsAfterDelay`). Every state gets a story, with the swap as its step. A variant that is only swapped to is defined anyway, so it can be a state |
| A hover link to another frame | A click on the layer, as above — hover-to-navigate is wrong on touch. The report names how many |
| Close links (`CLOSE`) | A `pressable` around the layer, with `action: "back()"`, which closes the overlay it sits on |
| Transition types (dissolve, move, push, smart-animate) | Flow `transition.name` (`dissolve`, `slide-*`, `push-*`, `shared`) |
| Overlay settings | `transition.overlay`, `dim`, `closeOnOutside` |
| Spring easing | `spring(mass, stiffness, damping)` with the same three numbers |
| Back links | A `pressable` around the layer, with `action: "back()"` |
| Links to a web address (`URL`) | A `pressable` around the layer, with `action: "openUrl(<address>)"`, which opens it in a new tab |
| Scroll-to links | A `pressable` around the layer, with `action: "scrollTo(<anchor>)"`, and an `anchor` on the layer it scrolls to |
| The prototype's starting frame | The project's home screen |

## What it approximates

| Source | Approximation |
| --- | --- |
| Figma constraints / absolute positions | Flex layout where possible; complex absolute stacks may shift |
| Text auto-height | Close, but not pixel-identical to Figma's text engine |
| Gradient geometry | Rebuilt from the transform matrix — handles are not stored by Figma, the matrix is |
| Text layers that differ from the style they name | Left with their own values and no `variant`, so they draw as designed; the report counts them |
| Layers whose shadow, gradient, gap or radius differs from the style or variable they name | Left with their own value; the report counts them |
| Shadows that follow a layer's shape | Drawn, but not bound to their effect style's token — the token is a box shadow |
| Styles in a file that defines no colours | The imported style starts as a copy of the default style's tokens and rules, with the file's own styles added, so what the screens leaned on in the default style is still there |
| Component instances that change structure or content | An instance that adds or removes a layer, replaces one with a layer of another kind, or changes a vector's path or an image's source, is expanded in place. It draws as designed but is a copy, not a reference; the report counts them |
| Variants no other variant matches | A variant that would have to change or remove what a nested component passes (its props, its own overrides), whose root is a layer of another kind, whose shared slot holds other content, or whose mask has another shape, stays a component of its own; the report counts them by reason |
| Components and component sets drawn on a design page | A screen each, tagged `figma-component` |
| A page that holds only components (a library page) | A component definition each, and no screen; a set becomes one component with a state per variant. A design page's instance of one references it |
| Frames of several widths that are one screen | Folded into one responsive screen where their structure agrees; kept apart where it does not |

## What it does not carry

Each of these is counted or named in the import report, unless marked
*not read*: those the importer does not look at yet, so the report says
nothing about them.

**Prototype**

- **Links to an address a wireframe will not open** — anything that is
  not a web address, mail, a phone number, a path or a fragment is counted
  and named in the report.
- **Links on a layer Figma hides** — a hidden layer is not drawn, so there
  is nothing to press and nothing is reported.
- **Links that wait for a drag** — the flow edge keeps the trigger, but
  no layer follows the link: a wireframe follows a link when it is
  clicked. Counted in the report by trigger. (Hover links and links after
  a delay are carried, as above.)
- **The undo-on-leave of a `WHILE_HOVERING` back, web address or
  scroll-to** (none of which can be undone) — counted in the report.
- **A second link after a delay on a frame or layer that already moves on
  after one** — counted in the report.
- **Links to the screen they are on**, and **a second link on a layer that
  already follows one when it is clicked** — counted in the report.
- **Links to a frame that is not a screen** — a component, a section, a
  frame on a library page — counted in the report.
- **Links from text to a layer that is neither on the screen nor a
  screen** — a section, a layer on another page — counted in the report.
- **Variant swaps a state cannot say** — a swap between a variant that
  stayed a component of its own and another, to a deleted variant, on a layer that stayed a copy or already
  goes to another screen on a click, and a hover on a component that also
  changes on a click — counted in the report by reason.
- **Set-variable actions** — a wireframe has no variables; counted in the
  report by the variable each one sets.
- **Scroll-to links with nowhere to go** — one whose target was deleted in
  Figma, or is not on the same screen.
- **Layers fixed to the screen that do not span it** — a floating button in
  a corner, or a fixed layer inside an auto-layout frame, scrolls with its
  page; the report counts them.
- **A sticky layer floating over an auto-layout frame** — one taken out of
  the frame's flow and placed by hand scrolls with the frame; the report
  counts them.
- **A sticky mark on a frame that is itself a screen** — *not carried*.
  Nothing around it scrolls, so there is nothing for it to stick in.

**Layout**

- **A screen's own minimum and maximum width** — *not read*. They say
  which widths the screen is for; a screen fills the device it is shown on.
- **A hugging layer's width in a font the host lacks** — a hug box is as
  wide as its text, and a fallback font sets it wider or narrower than
  Figma's did. The same holds for an Auto line height: `normal` is the
  fallback font's line, not the design font's.
- **Grid auto-layout** — *not read*. A frame laid out as a grid imports by
  the positions Figma drew its children at.
- **Layout grids and guides** — *not read*. They are a drawing aid, not
  part of the design.
- **Paragraph and list spacing** — *not read*.
- **Layers nested more than 40 deep** — omitted; the report names the screen
  and counts the subtrees. A guard against a runaway tree, not a budget:
  the deepest real design in the corpus goes 14.
- **A frame or group used as a mask** — it clips to its content, which is
  not one outline. Those layers render unclipped and the report counts
  them. So does a curved mask under a skew.

**Styles and variables**

- **String and boolean variables** — they do not become tokens.
- **Number variables with more than one mode** — a breakpoint set has no
  one value.
- **Colour variables with modes other than light and dark** — left as the
  colour the layer was drawn with.
- **Font size from a number variable** — the variable is a token, but the
  font size stays a plain number.
- **Effect styles that are only a blur** — no token; the blur is still drawn.
- **Fonts the host cannot fetch** — the `.fig` carries no font files. A
  family on Google Fonts loads; a commercial face falls back unless its
  file is handed over at import (see Import, below). Even in a fallback,
  a text at Auto line height stands at the height Figma recorded for its
  font.

**Components**

- **Overrides on a component from another file whose layer the copy does
  not carry** — an override names the library's own layer ids, and the
  local copy of the component carries those too, so text, colour, stroke,
  opacity, radius and interaction overrides land on library instances as
  on local ones. A size is not taken from such an override: the layer's
  own size is what Figma resolved for that instance. The few that name a
  layer the copy no longer holds — a layer since removed from the
  library, or one of a swapped-in component — are counted in the report
  (in one large client file, 832 of 27,998).
- **A component-library page** — imports as components, not screens: one
  definition per component on the page, a set as one component with a
  state per variant. A file that is only a library imports with no screen.
  The page is listed in the picker with its component count and no tiles.

**Not in a Figma file at all**

- **Per-layer appear animations**, **loop animations** (`marquee`, `float`,
  `spin`, …) and **parallax** — Figma has none of these; author them after
  import.
- **Comments, dev-mode annotations and section status** — *not read*.

Duplicate prototype edges from the same pair of frames collapse to one flow.

## Import

Import lives in the product, not in the open-source packages. There are two
ways in, and both give an ordinary Framewerk document you can render, edit
and export like any other:

- **A file.** Save a local copy from Figma (`.fig`) and drop it on the
  new-project dialog.

  Customize lists the **fonts** the file sets type in, with how many
  layers want each and whether this browser draws it. For one it does
  not — a commercial face — drop its `.woff2`, `.woff`, `.ttf` or `.otf`
  there, or, in Chrome and the desktop app, take it from the fonts
  installed on the machine. The file is stored with the project and
  becomes a face of the imported style, so every screen set in that
  family draws in it. The licence stays yours: nothing is fetched from
  anywhere.

  The file's styles can also seed a **brand**. When they suggest one — a
  colour the file calls primary (or, failing a name, its most used
  colour), the faces its headings and body are set in, how round its
  corners are — the import report says so, and Customize offers "Create
  a brand from this file's styles", unticked. The tick shows what the
  brand would hold, and its tooltip why each value was chosen. Nothing is
  added unless you tick it.
- **A paste.** Copy a frame in Figma and paste into Airframe. A Figma copy
  carries the selection and the components it uses; its images are links
  that expire after a few days, so paste soon after copying. Where you
  paste decides what it becomes:
  - in the new-project dialog, a new project;
  - with a project open, a new screen on the open page;
  - with the component editor open, a component;
  - on a board, a wireframe where you are looking.

  Components the copy uses are added to the project. One the project
  already has under the same name is reused when it is the same component;
  otherwise the pasted one is renamed and the project's own is left alone.

  The styles the copy's layers use come too — colours, gradients and
  shadows as tokens, text styles as rules — and join the style the project
  is drawn in, by the same rule: an identical one is reused, a clashing one
  is renamed and the pasted layers follow the new name, and the project's
  own is never overwritten. A project drawn in the built-in default style
  is given a style of its own, started from the default, to hold them.

  A style copied on its own from Figma's styles panel pastes as that
  style: a token (or, for a text style, a rule) added to the style the
  open project is drawn in. In the new-project dialog it is refused — a
  style alone is nothing to make a project of.
