A Figma plugin is a JavaScript (or TypeScript) program that runs in a sandboxed environment inside the Figma desktop or web app, with access to a specific API surface that lets it read and modify the current document — nodes, layers, styles, text, and so on. It is not a website embedded in an iframe, and it is not a script you paste into a browser console. It’s a small, self-contained application with its own manifest, its own permissions, and its own execution model that differs from ordinary web development in a few important ways. Understanding that distinction early saves a lot of confusion later, because most of the setup friction beginners hit comes from expecting plugin code to behave like a normal webpage script.
This guide walks through the process in order, from an empty folder to a plugin running inside a real Figma file. Each step builds on the last, so it’s worth following them in sequence even if some feel obvious.
Step 1: Understand the Two-Part Architecture Before Writing Any Code
Every Figma plugin has two separate execution contexts, and conflating them is the single most common source of early bugs.
The main thread (sometimes called the “sandbox”) runs your code.js file. It has direct access to the Figma document — you can create shapes, read text content, change fills, traverse the layer tree. It does not have access to the browser DOM. No document.getElementById, no fetch in older API versions without explicit permission, no direct HTML rendering.
The UI thread is an actual HTML page, rendered in an iframe, that you build like a normal (if very small) web app. It can use the DOM, CSS, and any front-end framework you like. It has no direct access to the Figma document.
These two threads talk to each other exclusively through postMessage. The UI sends a message to the main thread saying “the user clicked this button with these values,” and the main thread sends messages back saying “here’s the data you asked for” or “done.” If your plugin needs both a document-modifying feature and a visible interface, you will be writing this message-passing logic almost immediately, so it’s worth sketching out mentally before you open an editor: what does the UI need to ask for, and what does the main thread need to send back?
Step 2: Set Up Your Development Environment
You need three things installed before starting: a code editor (Visual Studio Code is the practical default, since Figma’s own tooling documentation assumes it), Node.js and npm, and the Figma desktop app — the plugin development workflow is noticeably smoother on desktop than in the browser.
Inside Figma, open the main menu, navigate to Plugins → Development → New Plugin, and choose a starting template. For a first plugin, pick either the plain “Run once” template or the “With UI” template if you already know your plugin needs a visible panel. Figma will prompt you to save a new folder on disk, and it auto-generates three files there: manifest.json, code.ts (or .js), and, if you chose a UI template, ui.html.
This scaffolding matters more than it looks like it does. It gives you a manifest that’s already correctly formatted, a code file wired to the right permissions, and a folder structure Figma already recognizes — which removes an entire category of “why won’t this load” problems that come from hand-writing a manifest incorrectly on the first attempt.
Step 3: Read the Manifest File Closely
Open manifest.json. It’s short, but every field controls something specific:
{
"name": "My First Plugin",
"id": "1234567890",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": ["figma"]
}
main points to the compiled JavaScript file that runs on the main thread — this is the entry point Figma actually executes. ui points to the HTML file rendered in the iframe, and it’s optional; omit it entirely if your plugin has no visible interface and just runs a transformation on selection. editorType determines whether the plugin appears in Figma design files, FigJam boards, or both, and getting this wrong is a common reason a plugin doesn’t show up where a beginner expects it to.
If you’re writing TypeScript, code.ts needs to be compiled into code.js before Figma will run it. The generated project includes a tsconfig.json and a build script for this — running npm install followed by npm run watch in the plugin folder keeps the compiled output updated automatically as you edit.
Step 4: Write a Minimal Version First
Resist the urge to build the full feature on the first pass. Start with something that proves the pipeline works end to end — Figma loading your plugin, your code running, and a visible result appearing in the document.
A reasonable first script:
const nodes = [];
for (let i = 0; i < 5; i++) {
const rect = figma.createRectangle();
rect.x = i * 150;
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 0.5, b: 0 } }];
figma.currentPage.appendChild(rect);
nodes.push(rect);
}
figma.currentPage.selection = nodes;
figma.viewport.scrollAndZoomIntoView(nodes);
figma.closePlugin();
This creates five orange rectangles in a row and closes the plugin immediately. It touches the core APIs beginners need constantly — node creation, property assignment, page manipulation, selection, and viewport control — without any UI complexity layered on top. Run it, confirm the rectangles appear, and only then move toward whatever the plugin’s actual purpose is.
Step 5: Add a User Interface, If Your Plugin Needs One
Plugins that operate silently on selection don’t need a UI at all — many of the most useful ones don’t. But if a user needs to type input, choose an option, or see a preview before committing a change, you’ll build an HTML interface in ui.html and show it from code.js:
figma.showUI(__html__, { width: 300, height: 200 });
Communication flows through postMessage in both directions. From the UI:
parent.postMessage({ pluginMessage: { type: 'create-shapes', count: 5 } }, '*');
From the main thread, listening for that message:
figma.ui.onmessage = (msg) => {
if (msg.type === 'create-shapes') {
// run your node-creation logic here, using msg.count
}
};
It’s worth treating this message contract as a small, deliberate API of its own — decide on message type values early, and keep the shape of the data consistent. Debugging a plugin where the UI and main thread disagree about what a message contains is tedious precisely because errors here often fail silently rather than throwing a clear exception.
Step 6: Test Inside a Real Figma File, Not Just the Console
Load your plugin into an actual working file through Plugins → Development → [Your Plugin Name], and test it against realistic content — nested frames, components, text with mixed styling, not just a blank canvas with a couple of shapes. Behavior that looks correct on simple test content sometimes breaks against real document structure, particularly around component instances and auto-layout frames, which have property behaviors that differ from plain rectangles and frames.
Figma’s developer console (accessible through the same Development menu) shows console.log output from the main thread, which is where most early debugging happens. UI-thread errors show up in the iframe’s own console instead, accessible by right-clicking the plugin UI and inspecting it — a detail that trips up a fair number of newcomers who only check one console and wonder why half their log statements never appear.
Step 7: Handle Errors and Edge Cases Before Calling It Done
A plugin that only works when the user has exactly the right thing selected isn’t finished — it’s a demo. Before considering a first plugin complete, check for the failure modes that show up immediately in real use:
- No selection at all. Does the plugin crash, or does it show a clear message telling the user to select something first?
- Wrong node type selected. If your plugin expects text nodes and the user selects a rectangle, does it fail gracefully?
- Locked or hidden layers. Figma’s API allows selecting these in some contexts, and attempting to modify them can throw errors your code should anticipate.
- Large selections. A loop that works fine on 3 nodes may be noticeably slow on 300 — worth a quick test before assuming performance is fine.
Wrapping the core logic in try/catch blocks and calling figma.notify() with a readable error message turns a silent failure into something the user can actually act on.
Step 8: Prepare for Publishing, Even If You’re Not Publishing Yet
Even a plugin built purely for personal or team use benefits from following the same conventions Figma requires for public publishing — it costs little now and saves a rewrite later if you decide to share it.
That means: a clear, specific name in the manifest rather than a placeholder; a description that states what the plugin does in one sentence; and an icon, if you’re aiming for the Community tab eventually. Figma’s publishing flow (Plugins → Development → Publish) walks through the remaining requirements — cover image, tags, category — only when you’re ready for that step, so there’s no need to prepare those until the plugin itself is stable.
A Quick Reference for the Full Sequence
| Step | What Happens |
|---|---|
| 1 | Understand the main-thread vs. UI-thread split |
| 2 | Install Node, VS Code, and Figma desktop |
| 3 | Read and understand manifest.json |
| 4 | Write a minimal script that proves the pipeline works |
| 5 | Add a UI and wire up postMessage communication |
| 6 | Test against real, complex Figma files |
| 7 | Handle empty selections, wrong node types, and errors |
| 8 | Fill in manifest metadata even before publishing |
Most first plugins stall somewhere between Step 4 and Step 6 — not because the idea is too ambitious, but because the two-thread architecture from Step 1 wasn’t fully internalized before writing began. If your plugin isn’t behaving the way you expect, that’s usually the first place to look back to.