Figma, step by step
This guide takes a designer and a developer from nothing to a running screen designed in Figma. How the pieces fit together, and the reference of what the library holds, is on Figma design-to-code. The pipeline is a preview in Mateu 3.0.
Who does what:
| Step | Who |
|---|---|
| 0. Get the Mateu UI Kit | anyone (the quick way) |
| 1. Build the library | developer (once per Mateu version) — or skip it with step 0 |
| 2. Publish it | whoever owns the team’s Figma |
| 3. Design screens | designer |
| 4. Convert the design | developer |
| 5. Serve it | developer |
0. Get the Mateu UI Kit (the quick way)
Section titled “0. Get the Mateu UI Kit (the quick way)”The library is published on Figma Community as Mateu UI Kit. Open it and Open in Figma (Figma calls it duplicating): you get your own copy of the components in your Drafts, with no plugin to build and on any Figma plan, the free Starter included. Then go to 2. Publish the library to share it with your team, or straight to 3. Design screens to design in that same file.
A duplicate is a copy: it does not receive later updates of the Community file. When a newer Mateu adds or changes components, duplicate the kit again (or build the library yourself, step 1, to match the exact Mateu version you run). The kit’s description says which Mateu version it matches.
1. Build the library (once)
Section titled “1. Build the library (once)”Build it yourself when you want the library to match your exact Mateu version, or to change it.
You need Figma Desktop on macOS or Windows — development plugins cannot be imported in the browser — and Node 20 or later.
-
Build the plugin from the Mateu repository:
Terminal window cd design/figma/pluginnpm cinpm run build # → dist/code.js -
In Figma Desktop, create a new, empty design file for the library — for example “Mateu library”. Use it only for this.
-
Main menu › Plugins › Development › Import plugin from manifest… and pick
design/figma/plugin/manifest.json. -
Run Plugins › Development › Mateu Library Generator › Generate the library. It creates (or replaces) one page per category —
Mateu · Field,Mateu · Layout,Mateu · Display,Mateu · Page Templates… — with one component or variant set per Mateu component. When it ends, it tells you how many properties and native slots it created.
- Optional but recommended: run Plugins › Development › Mateu Library Generator › Mateu Library
Self-test. It adds a
Mateu · Self-testpage that tries every component — every slot takes a child, every property is editable, every variant switches, every component is publishable — and draws a table of the results at the top. Any row in red is something to report.
At the bottom of many components you will see one or two rows of small grey text: the props
row (fieldId, actionId, route…, each showing its value — its own name until you set it) and
the flags row (required, readOnly…, each shown only while it is on). They mirror the
component’s properties: Figma only publishes a component whose properties all drive a layer, so
every property has one. They are never read as content; leave them as they are.
2. Publish the library
Section titled “2. Publish the library”- Move the library file into a team project and Assets › Publish library. Publishing a team library needs a Figma Professional, Organization or Education plan. The publish dialog must list all ~160 components (standalone components plus variant sets); if it lists fewer, see If something does not look right.
- In each design file: Assets › Libraries and enable “Mateu library”.
On the free Starter plan there are no team libraries. Design in the library file itself, on a
page of your own (call it “Screens”, say): the converter skips the Mateu · … pages, so only your
screens are read. Do not re-run the generator on that file once it holds screens without moving
them first.
After updating Mateu: rebuild the plugin (step 1.1), re-run Generate the library in the library file, re-publish, and accept the library update in every design file (Figma offers it in Assets). Instances placed with an older library keep working — see Older libraries.
3. Design screens
Section titled “3. Design screens”In your design file, with the library enabled:
- One top-level frame per page. Its name becomes the YAML file name: a frame
Ordergivesorder.yaml. A frame whose name starts with_is a draft and is skipped. - Use library instances only (from the Assets panel). Free-drawn rectangles and vectors are decoration and are ignored; plain frames are layouts (horizontal or vertical, by their auto-layout); a plain text is a text (a heading from 18 px).
- Slots — drag into them. The dashed areas of a component are slots: a Section’s content, a page template’s toolbar, body, KPI band… Drag library instances into a slot, or select it and use its +. The toolbar slot offers the Button first. The grey caption inside a slot is only a hint; delete it or leave it. The order inside the slot is the order on the page.
- Properties — the right panel. Select an instance: its labels, titles and config are in the
right panel. Set
fieldId(the field’s id in the page state),label,required,readOnlyfor fields;actionIdorroutefor buttons (people/${state.id}/edit);rowsSource.reffor a listing;stereotypeon a text or number field (email,money…) andoptionsSource.refon a select or radio group, the name of the source its options come from… Variants (style, size, theme) are there too. A text left as its placeholder (Label,Title) is not read. - Page templates. For a standard page, start from a
Mateu/Page Templates/<type>instance (ItemOverviewPage,WelcomePage,DashboardLandingPage…), as the screen itself or inside the screen frame, and fill its slots. A screen without a template is a free page: aMateu/Page/Headerinstance gives it a title and a toolbar. - Shared blocks. A frame named
partial:<name>is written once topartials/<name>.yamland referenced from every page that contains it. - Notes. Hidden layers and layers whose name starts with
#are never read.
Select and radio options
Section titled “Select and radio options”A Mateu/Field/Select (in every variant: select, combobox, searchable, treeSelect, listBox,
multiSelect) and a Mateu/Field/RadioGroup have an options property in the right panel. Type
the choices there, separated by commas:
| You type | The field offers (value → label) |
|---|---|
Spain, France, Portugal | spain → Spain, france → France, portugal → Portugal |
es=Spain, fr=France | es → Spain, fr → France |
pcs=Pieces; box=Boxes of 12, sealed | pcs → Pieces, box → Boxes of 12, sealed |
- A plain label gets its value made from it: lower case, accents dropped, spaces and symbols turned
into
-(Opción A→opcion-a,United Kingdom→united-kingdom). - Write
value=Labelwhen the value must be a particular code (es=Spain). Both kinds can be mixed. - When a label has a comma in it, separate the items with
;instead: as soon as the text has a;, only;separates. - The value is what the form keeps and sends when the user picks the option.
- Left as it comes (it shows
options, its own name), the field has no options of its own; a developer can still give it some in the YAML (options:oroptionsSource:). - The option texts the component draws (the RadioGroup’s “Opción A / Opción B”, the Select’s “Value”) are only a picture: they are never read.
The converter warns about an item it cannot read (a= with no label, a label with no letters or
digits such as ★, the same value twice) and leaves that item out.
Older libraries
Section titled “Older libraries”A #config line inside an instance (fieldId=email; required=true) — what the plugin writes for a
prop Figma refuses as a property — is read too; a property, when both exist, wins. Fields go INTO a
Section’s content slot: a layer next to a Section is not its content.
4. Convert the design
Section titled “4. Convert the design”The token
Section titled “The token”-
In Figma: Settings › Security › Personal access tokens › Generate new token, with the scope File content: Read only — nothing else.
-
Keep it in a file only you can read, never in the repository:
Terminal window mkdir -p ~/.mateu-secretspbpaste > ~/.mateu-secrets/figma-token # or paste it with an editorchmod 600 ~/.mateu-secrets/figma-token -
Never commit the token, and never paste it in a chat, an issue or a pull request. Revoke it in the same Figma settings when you are done.
The two commands
Section titled “The two commands”The file key is the part of the design file’s URL after /design/ (or /file/):
https://www.figma.com/design/<FILE_KEY>/My-screens.
# 1. download the design file as JSONcurl -sf -H "X-Figma-Token: $(cat ~/.mateu-secrets/figma-token)" \ "https://api.figma.com/v1/files/<FILE_KEY>" > design.json
# 2. write the pages into your app's specs/ui (Node 20+, nothing to install)node design/figma/to-yaml/bin/figma-to-yaml.mjs design.json --out src/main/resources/specs/uiIt prints one line per file written and one per warning. A warning is something the converter left out because the schema does not describe it — an unknown property, a value with no YAML counterpart, a Java-only page template — and says which layer it comes from; fix it in Figma and convert again.
Add --clean to delete, first, the pages a previous conversion wrote (files that start with the
converter’s # Generated from the Figma frame … header). Hand-written files in the same folder —
routes.yaml, sources.yaml, the app shell — are never touched. Do not edit a generated file:
change the design and convert again.
5. Serve the pages
Section titled “5. Serve the pages”A generated order.yaml is a page definition. In an app created from the YAML starter (see the
YAML quickstart), register it in specs/ui/routes.yaml:
routes: - route: order layout: order.yamland, to reach it from the menu, add an entry to the app shell (specs/ui/app.yaml):
menu: - type: RouteLink label: Order route: orderA button’s actionId only names an action. A generated page has no code behind it, so an action
nobody declares does nothing when clicked. Declare it in your own specs/ui/actions.yaml (see
the action catalogue), which the converter never touches, or bind a
view model to the route; the converter warns about every id it cannot find there. required fields
are enforced by an action that declares validationRequired: true:
type: Actionsactions: - id: save validationRequired: true restAction: source: {ref: contact-save} successMessage: SavedWhat a design draws as a value is an illustration. The Value of a PropertyRow or a
PlainText is not written to the YAML: the real value comes from the page state (a route’s state:
or data:, a view model). Use a Mateu/Display/Text for text that really is fixed.
Then run the app (mvn spring-boot:run) and open http://localhost:8080/order. The same files work
on a .NET or Python backend, or as a static bundle with no backend. What a
design cannot say — the REST sources that feed a listing, the routes, actions beyond actionId /
route — goes in your own files: see Routes and mounts and
Data sources.
6. Publish straight from Figma, with no backend
Section titled “6. Publish straight from Figma, with no backend”A design can be the UI: a static site with no server behind it, published from inside Figma. The
Mateu Publish plugin (design/figma/publish/plugin) reads the open file, needs no Figma token,
no Java and no repository, and uploads the site to Netlify or Cloudflare Pages, or gives you a zip.
- Install Mateu Publish from Figma Community. To run your own build instead:
cd design/figma/publish && npm ci && npm run build, then importplugin/manifest.jsonin Figma Desktop (Plugins › Development › Import plugin from manifest…). A file you can edit is enough: a file you can only view has to be duplicated first. - Run it in your design file. The panel starts from a working proposal for what a design cannot say:
- Screens and routes: one per top-level frame. A frame answers the route the screens navigate
to with a parameter:
artworks/${row.id}is answered by the frameartwork, atartworks/:id. - Data: the rows of each listing. They are published as a file of the site, or read from a public API if you give its URL. A listing over a file shows no search box: a file cannot search.
- What each button does: confirm with a message, go to a screen, or send the form to a URL. A primary button validates the required fields first.
- Screens and routes: one per top-level frame. A frame answers the route the screens navigate
to with a parameter:
- Choose where to publish.
What the design draws as a field’s value (a PropertyRow’s Value) is the data here: there is no
other source for it.
Netlify: create a personal access token (User settings › Applications). The next publish updates the same site. The site’s title comes from the panel’s Title field, which starts as the file’s name.
Cloudflare Pages: Cloudflare’s API cannot be called from a Figma plugin (it does not allow cross-origin calls), so the plugin goes through a small Worker that lives in your own account:
- Create a Worker on the free plan and paste
design/figma/publish/cloudflare-relay/worker.js. It stores nothing and forwards only the Pages API. - Create an API token with Cloudflare Pages: Edit.
- In the panel, give the Worker’s URL, your account ID, the token and a project name.
If your network cannot reach workers.dev, or you would rather not run a Worker, choose Download a
zip and drop it on a Cloudflare Pages project.
Tokens stay on your device (Figma’s client storage), never in the Figma file. Routes, data and button settings do travel in the file, so write nothing secret there.
Check the whole pipeline (end to end)
Section titled “Check the whole pipeline (end to end)”Before you design a real app, it is worth checking once that every link of the chain works for you: the kit in your Figma, the download, the converter and the running app. There are three ways, from the most hands-on to the fully automatic.
By hand, the first time
Section titled “By hand, the first time”- Get the kit: duplicate the Mateu UI Kit (step 0).
- Design one screen: on a new page of that file, add a frame called
contact. Into it drag aMateu/Page/Headerinstance (set itstitleto “New contact” and drop aMateu/Actions/Buttoninto its toolbar slot), then aMateu/Layout/Sectionwith twoMateu/Field/TextFieldinstances in its content slot. In the properties panel give each field afieldId(name,email) and alabel, and switchrequiredon foremail. - Import it: make a token, then run the two commands with
the file’s key, writing into the
specs/uifolder of an app made from the YAML starter. - Run it: add the route (
- route: contact/layout: contact.yaml) toroutes.yaml, start the app and openhttp://localhost:8080/contact. - What to check: the converter printed no warning, and the page shows the title, the button,
the section and both fields, with “Email” marked as required. The button does nothing yet: what
an
actionIddoes is the developer’s part (an entry inactions.yaml, see below); once it is declared withvalidationRequired: true, pressing it with Email empty shows a validation message.
With the plugin’s Self-test and Sample Screens commands
Section titled “With the plugin’s Self-test and Sample Screens commands”The plugin (step 1) has two commands that do this at scale:
-
Mateu Library Self-test checks the library itself — run it in the library file: every component, slot, property and variant, and whether Figma will publish every component (see Checking the library).
-
Mateu Sample Screens checks the library the way a designer uses it. Run it in any file that has the kit — a duplicate of the Community file, or a file with the published library enabled. On a page
Screens (sample)(replaced on each run) it builds six screens out of library instances only, exactly as a designer would: instances dropped into slots, ids and labels set in the properties panel, variants picked:Frame What it is contacta free form: a page header with Save and Cancel, two sections with a text field, a required email, a select, a date, a money amount and a checkbox artworkan ItemOverviewPage: a photo, the key facts as property rows, two tabsartworksa Listingreading the sourceartworks, searchable, whose rows openartworks/${row.id}dashboarda DashboardLandingPage: three metric cards and two panelsonboardinga GuidedProcessPagewith three stepswelcomea WelcomePagewith three calls to action over two tilesWhen it ends it says how many instances it made and whether anything was refused; the plugin console has the details. If it says the kit is not in the file, duplicate the kit (or enable the library) and run it again.
Download that file and convert it like any design: the six pages must come out with no warning.
What CI checks, and refreshing it from a real file
Section titled “What CI checks, and refreshing it from a real file”The repository runs the same six screens on every pull request that touches the Figma side
(design/figma/e2e, job figma-e2e): each design/figma/e2e/*.figma.json is converted to YAML,
served by a small YAML app on the Mateu release the YAML starter uses, and checked in a browser —
every screen renders with no error, Save is refused while a required field is empty and then sends
the form, the listing shows rows and a row opens its record, the search narrows the rows, the tabs
and the guided process’s steps switch, and the calls to action navigate.
The app around the pages is what a developer adds to any design: routes.yaml (one route per frame,
the record route fetching the row with data:), sources.yaml (the artworks source the listing
names) and actions.yaml, which says what the designer’s actionId: save does — validate the form
and post it. A designer names an action; a developer defines it.
To run it yourself (JDK 21, Maven and Node 22 or later):
cd design/figma/e2enpm ci && npx playwright install chromiumnpm test # the fixtures convert, with no warningnpm run e2e # convert, build, start on a free port, run the browser checksnpm run e2e:serve # the same app, left running, to look at the screens yourselfThe committed sample-screens.figma.json is built from the plugin’s own plan of the six screens
(npm run fixture rewrites it). To check a REAL file instead — one where you ran Mateu Sample
Screens, or composed the six frames yourself — download it as the real fixture:
npm run e2e:refresh -- https://www.figma.com/design/<FILE_KEY>/... # or just the file keyIt reads the token from FIGMA_TOKEN, else ~/.mateu-secrets/figma-token, and never prints it. It
writes downloaded-sample-screens.figma.json (only the page with the screens and the components they use),
and npm test and npm run e2e then check both fixtures. The frames must be named contact,
artwork, artworks, dashboard, onboarding and welcome.
The sample’s country select takes its choices from its options property
(Spain, France, Portugal, United Kingdom, see Select and radio options).
The browser check opens it, sees those four options, picks France and checks that Save posts
country: france.
If something does not look right
Section titled “If something does not look right”- Publish (or Assets) shows fewer components than the library has: Figma silently skips a
component with a property bound to no layer, or a text property with an empty default. A library
generated before the props/flags rows has that problem (it published 88 of 161). Rebuild the
plugin, re-run Generate the library, then run the Self-test: its Publish column must say
yesfor every row, and the toast countspublishable N/N. ANOnames the property at fault. - A warning names a layer: the layer’s name and node id are in the message; find it in Figma
with the node id in the URL (
?node-id=). - A component cannot take children or a property does not appear: re-run Generate the library with the current plugin, re-publish and accept the update; then run the Self-test and check its table.
- A warning says an instance “is not a Mateu component”: a component the current Mateu has
removed (
ContentLayout, for example, which older copies of the kit still draw). Replace it in the design with what its page recommends. - Slot content is missing from the YAML: send the developers the downloaded
design.json(it contains no token), so the converter can be checked against it.