figma-starter

v1.1.0

Turns a Figma section's screens into Figma-derived specs — per-screen spec.md files, app-level user-stories.md, and a build-order.md — and turns an OpenAPI/Swagger document into an entity model plus per-page UI-to-API bindings, then hands off to /speckit.specify.

Community extension — Independently maintained. Use at your own discretion. Learn more

Figma Starter — a Spec Kit extension

A GitHub Spec Kit extension that turns a Figma section's screens into Figma-derived specs — per-screen spec.md files, app-level user-stories.md, and a build-order.md — and turns an OpenAPI/Swagger document into an entity model plus per-page UI ↔ API bindings, then hands off to core /speckit.specify.

Quick start

specify init .                              # initialize in the current folder
specify init folder_name --integration [choose your agent]   # or create a new folder
specify extension add figma-starter \
  --from https://github.com/OmneWave/spec-kit-figma-starter/archive/refs/tags/v1.1.0.zip

It adds two commands — one for the design side, one for the data side:

/speckit.figma-starter.import       <figma-section-url>   [-o <module-name>]
/speckit.figma-starter.import-api <swagger-url-or-file> [-m <module-name>] [--out <folder>] [--page <slug>]
CommandAnswers
importWhat does the user see? — screens, flows, layout, user stories
import-apiWhat data sits behind it? — entities, and which endpoint backs which element

Run import first — import-api binds endpoints to the screens it produced, so it needs them to exist.

Agent-agnostic: the commands and pipelines are plain Markdown, so Spec Kit registers them for whichever agent you picked at specify init (Claude, Cursor, Copilot, Gemini, Windsurf, …). Icon/embedded-image export and the API document fetch run through bundled standard-library python3 scripts — no pip install, no uv, no extra CLI.

What makes this different from other Figma extensions: it is screen/image-driven. It pulls the actual rendered frames, traces prototype navigation into a flow map, and emits one spec per screen in build order — rather than only grounding generation in design tokens/context. The API side follows the same principle: it binds endpoints by reading the rendered screens, since a table's column headers and a form's field labels are the strongest evidence of which entity fields a page actually uses.

Where it fits

Both commands sit between /speckit.constitution and /speckit.specify — turning your design and your API into the specs that /speckit.specify then consumes:

specify init                    create a Spec Kit project
        │
/speckit.constitution           establish project principles
        │
/speckit.figma-starter.import      👈 THIS EXTENSION — Figma section → per-screen specs
        │
/speckit.figma-starter.import-api  👈 THIS EXTENSION — Swagger → entities + per-page bindings
        │
/speckit.specify                synthesize both into a feature spec
        │
/speckit.plan  →  /speckit.tasks  →  /speckit.implement

import-api is optional, but when you do run it, it runs second: it binds endpoints to the page folders import created, and stops if they are not there.

You don't have to run /speckit.constitution first. Neither command needs one, so you can install the extension and run import right away. If no constitution exists yet (.specify/memory/constitution.md), the command asks whether you'd like to set one up with /speckit.constitution before it hands off to /speckit.specify — so you end up with a constitution either way:

/speckit.figma-starter.import     run right after install — Figma section → per-screen specs
        │
   (no constitution yet?)  →    "Set one up with /speckit.constitution first?"  →  /speckit.constitution
        │
/speckit.specify  →  /speckit.plan  →  /speckit.tasks  →  /speckit.implement

Getting started

Step 1 — Install Spec Kit and initialize a project

If you don't already have a Spec Kit project, install the CLI and run specify init. Pick your agent with --integration (e.g. claude, cursor, copilot, gemini, windsurf). Check the Spec Kit repo for the latest release tag.

# install the Spec Kit CLI (replace the tag with the latest release)
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@v0.12.14

# create a project, then cd into it
specify init my-app --integration claude
cd my-app

Step 2 — Add this extension

From your Spec Kit project root, install directly from a release archive:

specify extension add figma-starter \
  --from https://github.com/OmneWave/spec-kit-figma-starter/archive/refs/tags/v1.1.0.zip

Or, for local development against a checkout of this repo:

specify extension add --dev /path/to/figma-starter

Verify it registered:

specify extension list   # should show "Figma Starter (v1.1.0)"

Note on the community catalog: the Spec Kit community catalog is discovery-only (install_allowed: false) — being listed there makes the extension searchable, but users still install with --from <url> above (or by copying the entry into their own catalog.json).

Step 3 — Set your tokens

Export them, or put them in a .env file at the project root. Without a Figma token the import pipeline falls back to the Figma MCP server. A Swagger token is only needed if your API document is behind auth.

export FIGMA_TOKEN=figd_...           # or FIGMA_ACCESS_TOKEN
export SWAGGER_TOKEN=...              # or API_TOKEN — only for a protected API document
# or: echo 'FIGMA_TOKEN=figd_...' >> .env

Instead of SWAGGER_TOKEN you can pass any header inline, repeatably: --header "Authorization: Bearer <token>".

You also need python3 >= 3.8 on your PATH — used for the REST asset pull and the API document fetch (standard library only, no pip install). A YAML OpenAPI document additionally needs pyyaml; JSON needs nothing.

Step 4 — Run the workflow

The recommended order establishes your project principles first, generates the Figma-derived specs with this extension, then hands off to core Spec Kit:

/speckit.constitution                                               # 1. project principles
/speckit.figma-starter.import      <figma-section-url> [-o <name>]  # 2. Figma → specs
/speckit.figma-starter.import-api  <swagger-url> -m <name>          # 3. Swagger → entities + bindings
/speckit.specify                                                    # 4. point it at figma-starter/<module>/ + api-specs/
/speckit.plan                                                       # 5. technical plan
/speckit.tasks                                                      # 6. task list
/speckit.implement                                                  # 7. build it

Use the same module name in both commands — import -o <name> writes that module folder, and import-api -m <name> reads it. The flags differ because their jobs do: -o names where import puts its output, while -m names the module import-api binds against. With the wrong name it finds no screens and stops, suggesting the name it thinks you meant. If figma-starter/ holds more than one module and you omit -m, it stops and lists them rather than guessing.

With several modules, give each one an output folder: --out <folder> renames api-specs/, and entities.md and api-catalog.md sit at the top of it describing the module that was run, so without it the second module's run overwrites the first's.

/speckit.figma-starter.import-api <swagger-url> -m storefront --out api-specs-storefront
/speckit.figma-starter.import-api <swagger-url> -m checkout   --out api-specs-checkout

The extra output folders sit beside the modules without ever being counted as one — a folder holding an .api-index.json is this pipeline's own — so -m stays optional on a single-module project no matter how many times you re-run. --out takes one folder name, not a path, and refuses to name a design module.

Prefer to start straight away? import works standalone:

/speckit.figma-starter.import <figma-section-url> [-o <name>]  # run right after install
# → if no constitution exists, the command asks whether to set one up with
#   /speckit.constitution before you continue to /speckit.specify
/speckit.specify  →  /speckit.plan  →  /speckit.tasks  →  /speckit.implement

Each command runs four steps in order and then stops, handing the result to /speckit.specify:

import — Figma section → per-screen specs

StepWhat it does
pull-screensDownload section frames as PNGs + prototype taps → screens.json
trace-flowsBuild a flow map (pages, dialogs, journeys)
read-screensLayout notes per page / dialog / step
write-specuser-stories.md, build-order.md, one spec.md per screen

Design tokens and typography come from the local Figma MCP (get_variable_defs, get_design_context); icons and embedded images come from the bundled REST helper in the background.

import-api — OpenAPI/Swagger → entity model + UI ↔ API bindings

StepWhat it does
pull-apiFetch the document and flatten it into api-specs/.api-index.json
derive-entitiesapi-specs/entities.md — the entity model the UI must carry
bind-uiMatch endpoints to UI elements, reading each page's spec and its screenshots, and chart the chains where one call is not enough
write-api-specOne api-specs/<NN>-<slug>/api-bindings.md per page, then api-specs/api-catalog.md summarising them

Its output lands in figma-starter/api-specs/, a sibling of the module folder laid out the same way — one folder per page, matching names. The module folder itself is read-only to this command. api-specs is the default name; --out <folder> gives a run its own output folder, which is what a multi-module project needs.

OpenAPI 3.x and Swagger 2.0 both normalize into the same index — $refs resolved, allOf chains merged, pagination envelopes (Page<T>) unwrapped to the element type.

Screens are a prerequisite, and build order is the order

There is no screenless mode. import-api binds endpoints to screens, so if figma-starter/ holds no module — or the module holds no page folder with a spec.md — it stops and tells you to run import first. It will not invent screens to bind against.

Given screens, the pages are worked through in the order build-order.md lays out: the order the user journeys run, which is rarely the order the folders are numbered. Binding Dashboard after Login makes it obvious where the session and the user id come from; binding it first makes that a mystery to solve twice.

Bindings are element-level, and gaps are the point

A binding names the element, when it fires, what it sends and where each parameter comes from, what the response fills, and whether a field actually carries what the screen shows:

ElementTriggerEndpointSendsPopulatesValue sourceNotes
Specification List / search fieldon typeGET /specificationsq ← field text200 Specification[] → table rows—Debounce expected; not stated in the document
Specification List / Created on rangeon change————Unbound: GET /specifications has no date parameters; only client-side filtering is possible today

A repeating row or card is one call, then a field map — the collection call stated once, followed by a table saying which field of one element fills which part of one row or card. Never one call per row. The same rule holds when one response fills a table and the summary cards above it: bound once, with a note naming what else it feeds, so nobody builds two round trips out of it.

Every endpoint a page uses also gets one worked example under ## Endpoint examples — the request, the headers that matter, the body, and a trimmed response — written once at first use however many sections call it.

Where nothing matches, that is recorded rather than papered over — unbound UI (an element with no endpoint, as above) and unused endpoints (endpoints the screens never call). On a generated back-office API those two lists are usually the most useful output of the whole run, and api-specs/api-catalog.md gathers both across every page once the run finishes.

Pass --page <slug> to redo one screen's bindings without touching the others — useful after a single spec.md changed. It accepts 01-specification-list, specification-list, or just 1. A page-scoped run leaves api-catalog.md alone: one screen is not evidence for what the whole module calls.

When one call is not enough

A binding row says which endpoint serves an element. It cannot say in what order — and on a generated API one call is often not enough: the id the call needs is itself the result of an earlier call, a displayed name has to be looked up from an id, a write has to run create-then-attach-then-submit. That is where implementations get invented, because the row looks bound and {loanId} turns out to come from nowhere.

So each page carries a ## Call sequences section — the one part of the output that is not a table, because an order, a branch, a parallel leg and a per-row fan-out do not fit in a row. Each chain gets a flowchart, with the value that moves labelled on every arrow and the branches the document actually declares:

flowchart TD
    T([on load]) --> A["GET /accounts/{accountNo}"]
    A -->|"accountId"| B["GET /accounts/{accountId}/loans"]
    A -.->|"404"| E[["Not-found state"]]
    B --> D{"loans empty?"}
    D -->|"yes"| Z[["Empty schedule state"]]
    D -->|"no — loanId"| C["GET /loans/{loanId}/schedule"]
    C -->|"Instalment[]"| UI[["Schedule table"]]

Under it, a step table making the chain checkable — every Needs has to resolve to the route, a user input, or an earlier step's Yields, and one that does not is reported as a finding rather than papered over:

StepCallNeedsYieldsBlocking
1GET /accounts/{accountNo}accountNo ← routeAccount.idblocks step 2
2GET /accounts/{accountId}/loansaccountId ← step 1.Account.idLoan.id where status = ACTIVEblocks step 3
3GET /loans/{loanId}/scheduleloanId ← step 2; page, size ← pagination stateInstalment[] → rows—

The payloads are not repeated here — each step links to that endpoint's example, so the chain shows only the value that carries from one call to the next.

Chains are charted the way they should be built, not the first way that works: server-side filters instead of fetching and discarding, one reference list fetched once and joined instead of one call per row, independent legs marked parallel. Where the document cannot complete a chain it stops at a MISSING node with what it would need, instead of inventing an endpoint. api-catalog.md lists every chain in the module in one table.

What the API carries vs. what the screen works out

A screen states more than an API stores, and that gap is where implementations get invented. So every element that shows a value is marked direct, derived, or static — and formatting a field is not deriving a value:

Payments completed 120/120 is direct — paidInstalments and totalInstalments, printed together. Account Health: Excellent on the same card is derived — nothing in the document holds Excellent, and nothing says where Excellent stops and Good begins.

Each derived value gets its own entry in that page's ## Derived values — where it shows, the minimal attribute set the rule reads with the call behind each, what has to change for the value to move, where the rule came from, a confidence of certain / fairly certain / uncertain, and the logic as pseudocode:

### Account Health

- Shown in:    Account Detail / Card: Account health / verdict
- Inputs:      Account.paidInstalments, .totalInstalments, .overdueAmount ← GET /accounts/{id}
- Changes when: a payment is posted, or an instalment falls overdue
- Source:      card legend
- Confidence:  uncertain

    if account.overdueAmount > 0: return "At risk"
    ratio = account.paidInstalments / account.totalInstalments
    if ratio >= 0.95: return "Excellent"
    if ratio >= 0.80: return "Good"
    return "Fair"

Every one of these is BFF work — a derivation belongs in the backend-for-frontend layer, not the component, and the spec says so. It does not invent a GET /bff/… route to put it behind: naming an endpoint the document never declared is exactly the failure the rest of the pipeline exists to prevent.

Only values that move with the record qualify — a label, a legend, or a total the pagination envelope already carries is not a derivation. Where the document cannot support a value at all, the entry says not derivable and what it would need, rather than naming a field that does not exist. api-catalog.md rolls these up across the module: the value, the screens showing it, and the calls behind it.

Output

The two commands write to two sibling trees with the same shape: import fills the module folder, and import-api fills api-specs/, one folder per page under each. A screen and the data contract behind it are one folder name apart, and neither command overwrites the other's work. api-specs below is the default output name — with --out it is whatever you called it, and a multi-module project has one per module.

figma-starter/                     ← under the project root
  <module>/                          the design source — written by import
    screens.json
    resources-manifest.json
    user-stories.md
    build-order.md
    design-tokens/tokens.json        module-level (shared)
    typography/typography.json       module-level (shared)
    01-<slug>/
      spec.md
      figma-resources/
        screens/01-<slug>.png
        icons/*.svg
        embedded/*.png
    ...
  api-specs/                       ← the data source — written by import-api
    .api-index.json              ← the normalized document — every later step reads this
    entities.md                  ← the entity model the UI must carry
    api-catalog.md               ← written last — every endpoint and which screens call it
    01-<slug>/                   ← mirrors the page folder of the same name
      api-bindings.md            ← entities, bindings, sequences, derived values, examples
    ...

import-api reads the module folder — page specs, screenshots, user stories — and writes only under api-specs/, so a re-run can never disturb a reviewed screen spec.

api-catalog.md is written last, once every page's bindings exist, and is a roll-up of them rather than a second rendering of the document: every endpoint with the screens that call it, the endpoints nothing calls, the UI nothing serves, the chains that have to be orchestrated, the values that have to be computed, and which screens touch each entity. Parameters, payloads, field lists, pseudocode and flowcharts stay out of it — .api-index.json holds those and the page files interpret them, so there is nothing here to drift. It is the one file that answers how much of this API does the design actually use.

From there, continue with core Spec Kit: /speckit.specify → /speckit.plan → /speckit.tasks → /speckit.implement. Point /speckit.specify at both figma-starter/<module>/ and figma-starter/api-specs/ for the whole module, or at one <NN>-<slug>/spec.md with its api-specs/<NN>-<slug>/api-bindings.md to build a single screen.

Layout of this repo

figma-starter/
├── extension.yml                 # Spec Kit manifest
├── commands/
│   ├── speckit.figma-starter.import.md
│   └── speckit.figma-starter.import-api.md
├── scripts/
│   ├── figma_pull.py             # self-contained stdlib REST helper (screens + resources)
│   └── swagger_pull.py           # self-contained stdlib fetch + normalize (OpenAPI 3.x / Swagger 2.0)
├── figma-images-to-spec/         # the design-side pipeline (installed alongside the command)
│   ├── SKILL.md  USER.md
│   ├── pull-screens/  trace-flows/  read-screens/  pull-resources/
│   └── write-spec/ (+ FORMAT-*.md)
├── swagger-to-spec/              # the data-side pipeline
│   ├── SKILL.md  USER.md
│   ├── pull-api/  derive-entities/  bind-ui/
│   └── write-api-spec/ (+ FORMAT-*.md)
├── README.md  LICENSE  CHANGELOG.md
├── .env.example  .gitignore  .extensionignore

Each pipeline is a folder of Markdown skills: an orchestrator SKILL.md, one <step>/SKILL.md per step, and FORMAT-*.md templates showing the exact shape of each output file. The agent reads a step's instructions only when it runs that step.

License

MIT — see LICENSE.

Stats

8 stars

Version

1.1.0release
Updated 24 days ago

Install

Using the Specify CLI

specify extension add figma-starter --from https://github.com/OmneWave/spec-kit-figma-starter/archive/refs/tags/v1.1.0.zip

Owners

License

MIT