⬡ FRAMEWERK
Integrations

Figma

What the Figma importer carries into a Framewerk document, what it approximates, and what it does not carry.

View as Markdown

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

SourceBecomes
Frames with childrenScreens (one per importable frame) with a layout tree
A top-level frameTwo 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 sizeA 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 sizeA 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 columnEvery 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 vectorspath (or svg with currentColor) — themes with the style
Multi-colour / gradient vectorsA container with an inline SVG background; gradients become <defs>
Imagesimage with an asset ref
Shadows and blursboxShadow (or filter: drop-shadow() where the shadow follows the layer's shape), backdropFilter for a background blur, filter for a layer blur
MasksClipping 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 structurePages grouping imported screens

Type

SourceBecomes
Text layerstext with fontSize, fontWeight, fontFamily, lineHeight, letterSpacing, case, decoration, alignment and colour
Line height set to AutoThe 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 layeraction: "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 itRuns inside the text, each with its own style
A link on a range of texthref 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 stylesA 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.

SourceBecomes
Solid fill styles and colour variablesA 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 modeOne 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 gradientA 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 variablesA 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 variablevar(--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 variableFour 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

SourceBecomes
Components used more than onceOne component definition and a component reference per instance. Where the instance sits and its size stay on the reference
Variant setsA 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 instanceA string prop on the component, with each instance's text as its value
A boolean property of a componentA 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 componentA 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 instancesWhere 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 columnflexShrink 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 libraryThe 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

SourceBecomes
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-tohoverAction 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 componentA 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 frameA 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 settingstransition.overlay, dim, closeOnOutside
Spring easingspring(mass, stiffness, damping) with the same three numbers
Back linksA 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 linksA pressable around the layer, with action: "scrollTo(<anchor>)", and an anchor on the layer it scrolls to
The prototype's starting frameThe project's home screen

What it approximates

SourceApproximation
Figma constraints / absolute positionsFlex layout where possible; complex absolute stacks may shift
Text auto-heightClose, but not pixel-identical to Figma's text engine
Gradient geometryRebuilt from the transform matrix — handles are not stored by Figma, the matrix is
Text layers that differ from the style they nameLeft 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 nameLeft with their own value; the report counts them
Shadows that follow a layer's shapeDrawn, but not bound to their effect style's token — the token is a box shadow
Styles in a file that defines no coloursThe 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 contentAn 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 matchesA 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 pageA 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 screenFolded 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.