Figma
What the Figma importer carries into a Framewerk document, what it approximates, and what it does not carry.
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_HOVERINGback, 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:
normalis 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
.figcarries 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,.ttfor.otfthere, 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.