Figma has revolutionized the design world with its collaborative, browser-based interface. But did you know you can extend Figma’s functionality by building your own plugins? Whether you’re a designer looking to automate repetitive tasks or a developer wanting to create powerful design tools, Figma plugins open up endless possibilities. In this guide, you will learn Learn how to build Figma plugins step-by-step. You will learn to create custom tools, automate workflows, and extend Figma using APIs, TypeScript, and UI.
Table of Contents
What Are Figma Plugins?
Figma plugins are custom extensions that run within Figma, allowing you to:
- Automate repetitive tasks – Save hours of manual work
- Process design data – Extract, analyze, and transform design elements
- Generate content – Create components, layouts, or entire designs programmatically
- Integrate external services – Connect with APIs, databases, or other tools including trusted VPN providers for secure remote connections and protected access to development resources.
- Enhance workflows – Build custom tools tailored to your team’s needs
External data can also become part of a plugin’s workflow when the use case requires information that does not exist inside the Figma document itself. For example, a plugin working with business or customer data could connect to a data enrichment API to retrieve additional company or contact details and use them within its interface or generated content.
Popular plugins like “Content Reel”, “Unsplash”, and “Iconify” have millions of users and demonstrate the impact a well-built plugin can have on the design community. For further details, check the official docs.
Why Build a Figma Plugin?
Building a Figma plugin is valuable for several reasons:
- Solve Real Problems – If you’ve encountered a workflow challenge, chances are others have too
- Learn New Skills – Gain hands-on experience with TypeScript, APIs, and asynchronous programming while strengthening your understanding of modern website design workflows and developer collaboration.These technical skills also prove valuable when building high-performing eCommerce experiences that work alongside a shopify ads agency to improve user journeys, attract qualified traffic, and increase conversions.
- Give Back to the Community – Share your solution with thousands of designers worldwide
- Career Growth – Showcase your problem-solving abilities and technical skills
- Potential Income – Figma allows you to publish paid plugins on its Community marketplace
Understanding Figma Plugin Architecture
Before we dive into building, it’s important to understand how Figma plugins work.
Two-Context System
Figma plugins run in two separate JavaScript contexts that communicate with each other:
1. Plugin Context (Sandbox)
- Runs your main plugin code (
code.js) - Has full access to the Figma Plugin API
- Can read and manipulate design elements
- Cannot access browser APIs (DOM, fetch, etc.)
- Runs securely in an isolated sandbox
2. UI Context (iframe)
- Runs your user interface code (
ui.html) - Has access to browser APIs and the DOM
- Cannot directly access Figma’s API
- Communicates with the plugin context via
postMessage
This separation ensures security while giving you the flexibility to build rich interfaces.
Communication Flow
User interacts with UI → UI sends message → Plugin receives message
↓
Plugin processes request
↓
Plugin sends result
↓
UI receives and displays
Learn more about this architecture in the official documentation.
What We’ll Build: Color Palette Extractor
Now that we understand the fundamentals, let’s build a practical plugin! We’ll create a Color Palette Extractor that solves a common designer problem: tracking and documenting all colors used in a design.
Our plugin will:
- 🎨 Extract all unique colors from selected frames and layers
- 📋 Display colors in a beautiful grid interface
- 📋 Enable one-click copying of color codes (including 8-digit HEXA for opacity)
- 🖼️ Generate visual color palette frames directly in Figma
Let’s build it step by step!
How To Build Figma Plugins?
To build Figma plugins, you start by creating a new plugin using the Figma Desktop App, which generates a basic project structure with files like manifest.json, code.ts, and ui.html. The plugin works using two contexts: the main code (which interacts with the Figma Plugin API) and the UI (which handles user interaction in the browser).
You then write logic to manipulate design elements, build a user interface, and enable communication between both contexts using postMessage. After coding, you compile your plugin, test it inside Figma, and once everything works, you can publish it to the community for others to use.
Now, let’s begin the guide.
Step 1: Create a New Plugin
Let’s get started! Figma makes it easy to create a plugin from scratch.
Requirements
Before you begin, make sure you have:
- Figma Desktop App – Download here (plugins require the desktop version, not the browser)
- Node.js (v14 or higher) – Download from nodejs.org
- Code editor – VS Code is recommended
- TypeScript/JavaScript knowledge
1.1 Create a plugin in Figma
Open the Figma Desktop app and follow these steps:
Plugins → Development → New Plugin…

1.2 Configure Plugin Details
Give your plugin a descriptive name: “Color Palette Extractor.”

1.3 Select Plugin Template
Choose “With UI & browser APIs” template. This gives us:
- A custom user interface
- Access to browser APIs (clipboard, DOM, etc.)
- TypeScript support out of the box
- Example code to get started

1.4 Choose Save Location
Select a folder on your computer where you want to save the plugin files.

1.5 Plugin Structure Created
Success! Figma has generated all necessary files for you.

Open the folder in VS Code. You’ll see these key files:
color-palette-extractor/
├── manifest.json # Plugin configuration
├── code.ts # Main plugin logic (Figma API)
├── ui.html # User interface (HTML/CSS/JS)
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies
File Descriptions:
manifest.json– Tells Figma about your plugin (name, UI file, permissions). Check the guide for more detailscode.ts– Your main plugin code that runs in Figma’s sandboxui.html– Your plugin’s user interface (UI context)tsconfig.json– TypeScript compiler settingspackage.json– Node.js dependencies and build scripts
Step 2: Install Dependencies
Open your terminal in the plugin directory and install dependencies:
npm installThis installs:
@figma/plugin-typings– TypeScript type definitions for the Figma Plugin APItypescript– TypeScript compiler
These packages provide type-checking and autocompletion in your code editor, making development much easier.
Step 3: Build the Color Extraction Logic
Now let’s write the core logic to extract colors from Figma nodes.
3.1 Update code.ts
Replace the contents of code.ts with:
figma.showUI(__html__, { width: 400, height: 600 });
function formatColor(r: number, g: number, b: number, opacity: number = 1): string {
const toHex = (value: number) => {
const hex = Math.round(value * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
if (opacity < 1) {
const alphaHex = toHex(opacity);
return `${hex}${alphaHex}`.toUpperCase();
}
return hex.toUpperCase();
}
function extractColors(node: SceneNode, colorSet: Set<string>): void {
if ('fills' in node && node.fills !== figma.mixed) {
const fills = node.fills as readonly Paint[];
fills.forEach(fill => {
if (fill.type === 'SOLID' && fill.visible !== false) {
const opacity = fill.opacity !== undefined ? fill.opacity : 1;
colorSet.add(formatColor(fill.color.r, fill.color.g, fill.color.b, opacity));
}
});
}
if ('strokes' in node && Array.isArray(node.strokes)) {
const strokes = node.strokes as readonly Paint[];
strokes.forEach(stroke => {
if (stroke.type === 'SOLID' && stroke.visible !== false) {
const opacity = stroke.opacity !== undefined ? stroke.opacity : 1;
colorSet.add(formatColor(stroke.color.r, stroke.color.g, stroke.color.b, opacity));
}
});
}
if ('children' in node) {
node.children.forEach(child => extractColors(child, colorSet));
}
}
function extractColorsFromSelection(): void {
const selection = figma.currentPage.selection;
if (selection.length === 0) {
figma.ui.postMessage({
type: 'no-selection',
message: 'Please select a frame or element to extract colors from.'
});
return;
}
const colorSet = new Set<string>();
selection.forEach(node => extractColors(node, colorSet));
const colors = Array.from(colorSet).sort();
if (colors.length === 0) {
figma.ui.postMessage({
type: 'no-colors',
message: 'No colors found in the selection.'
});
return;
}
figma.ui.postMessage({
type: 'colors-extracted',
colors
});
}
figma.ui.onmessage = (msg: { type: string; colors?: string[] }) => {
if (msg.type === 'extract-colors') {
extractColorsFromSelection();
return;
}
if (msg.type === 'create-palette') {
if (!msg.colors || msg.colors.length === 0) {
figma.notify('No colors to create palette from.');
return;
}
const colors = msg.colors;
(async () => {
try {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }).catch(() =>
figma.loadFontAsync({ family: 'Roboto', style: 'Regular' })
);
const paletteFrame = figma.createFrame();
paletteFrame.name = 'Color Palette';
paletteFrame.resize(colors.length * 120 + 40, 180);
paletteFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
paletteFrame.x = figma.viewport.center.x - paletteFrame.width / 2;
paletteFrame.y = figma.viewport.center.y - paletteFrame.height / 2;
colors.forEach((colorStr, index) => {
let r: number, g: number, b: number, opacity: number = 1;
if (colorStr.length === 9) {
r = parseInt(colorStr.substring(1, 3), 16) / 255;
g = parseInt(colorStr.substring(3, 5), 16) / 255;
b = parseInt(colorStr.substring(5, 7), 16) / 255;
opacity = parseInt(colorStr.substring(7, 9), 16) / 255;
} else {
r = parseInt(colorStr.substring(1, 3), 16) / 255;
g = parseInt(colorStr.substring(3, 5), 16) / 255;
b = parseInt(colorStr.substring(5, 7), 16) / 255;
}
const rect = figma.createRectangle();
rect.name = colorStr;
rect.resize(100, 100);
rect.x = 20 + index * 120;
rect.y = 20;
rect.fills = [{ type: 'SOLID', color: { r, g, b }, opacity }];
rect.cornerRadius = 8;
paletteFrame.appendChild(rect);
const text = figma.createText();
text.x = 20 + index * 120;
text.y = 130;
text.resize(100, 30);
text.fontSize = 12;
text.characters = colorStr;
text.textAlignHorizontal = 'CENTER';
text.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }];
paletteFrame.appendChild(text);
});
figma.currentPage.appendChild(paletteFrame);
figma.currentPage.selection = [paletteFrame];
figma.viewport.scrollAndZoomIntoView([paletteFrame]);
figma.notify(`Created palette with ${colors.length} colors!`);
} catch (error) {
figma.notify('Error creating palette. Please try again.');
console.error('Palette creation error:', error);
}
})();
return;
}
if (msg.type === 'copy-colors') {
figma.notify('Colors copied to clipboard!');
return;
}
};
if (figma.currentPage.selection.length > 0) {
extractColorsFromSelection();
}Understanding the Code
formatColor() function:
- Converts Figma’s RGB values (0-1 range) to HEX format
- Handles opacity by adding an alpha channel (8-digit HEXA)
- Example:
rgba(233, 52, 52, 0.6)→#E9343499
extractColors() function:
- Recursively traverses the node tree
- Extracts colors from both fills and strokes
- Stores unique colors in a
Set(automatic deduplication) - Handles nested groups and frames
extractColorsFromSelection() function:
- Gets the current selection
- Calls
extractColors()for each selected node - Sends results back to UI via
postMessage
Message handler:
- Listens for events from the UI
- Handles “extract-colors”, “create-palette”, and “copy-colors” actions
Step 4: Build the User Interface
Now let’s create a beautiful UI for our plugin using Tailwind CSS for cleaner, more maintainable styling.
4.1 Why Tailwind CSS?
Tailwind CSS is a utility-first CSS framework that provides:
- Better code organization – No need to write custom CSS classes
- Smaller file size – Only includes styles you actually use
- Consistent design – Pre-defined spacing, colors, and utilities
- Faster development – Style directly in HTML with utility classes
4.2 Update ui.html
Replace the contents of ui.html with:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="<https://cdn.tailwindcss.com>"></script>
<style>
.colors-grid::-webkit-scrollbar {
width: 8px;
}
.colors-grid::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 4px;
}
.colors-grid::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.colors-grid::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>
</head>
<body class="font-sans p-4 bg-white text-gray-800">
<div class="mb-5">
<h2 class="text-lg font-semibold mb-2 text-black">🎨 Color Palette Extractor</h2>
<p class="text-[13px] text-gray-600">Extract colors from selected frames and layers</p>
</div>
<div class="flex gap-2 mb-5">
<button
class="flex-1 px-4 py-2.5 border-0 rounded-md text-[13px] font-medium cursor-pointer transition-all duration-200 bg-blue-600 text-white hover:bg-blue-700"
id="extract-btn">Extract Colors</button>
<button
class="flex-1 px-4 py-2.5 border-0 rounded-md text-[13px] font-medium cursor-pointer transition-all duration-200 bg-gray-100 text-gray-800 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
id="create-palette-btn" disabled>Create Palette</button>
</div>
<div class="hidden p-3 rounded-md mb-4 text-[13px]" id="status"></div>
<div class="hidden" id="colors-container">
<div class="flex justify-between items-center mb-3">
<span class="text-[13px] font-semibold text-gray-800" id="colors-count">0 colors found</span>
<button class="px-3 py-1.5 bg-gray-100 border-0 rounded text-xs cursor-pointer text-gray-800 hover:bg-gray-200"
id="copy-btn">Copy All</button>
</div>
<div class="colors-grid grid grid-cols-3 gap-4 max-h-[400px] overflow-y-auto p-1 pb-2" id="colors-grid"></div>
</div>
<div class="text-center py-[60px] px-5 text-gray-400" id="empty-state">
<div class="text-5xl mb-4">🎨</div>
<div class="text-sm leading-relaxed">
Select a frame or element and click<br>"Extract Colors" to get started
</div>
</div>
<script>
let currentColors = [];
function showStatus(message, type = 'info') {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
// Remove all status classes
statusEl.classList.remove('hidden', 'bg-blue-50', 'text-blue-700', 'border', 'border-blue-200',
'bg-orange-50', 'text-orange-700', 'border-orange-200',
'bg-green-50', 'text-green-700', 'border-green-200');
// Add appropriate classes based on type
if (type === 'info') {
statusEl.classList.add('bg-blue-50', 'text-blue-700', 'border', 'border-blue-200');
} else if (type === 'warning') {
statusEl.classList.add('bg-orange-50', 'text-orange-700', 'border', 'border-orange-200');
} else if (type === 'success') {
statusEl.classList.add('bg-green-50', 'text-green-700', 'border', 'border-green-200');
}
statusEl.classList.remove('hidden');
setTimeout(() => statusEl.classList.add('hidden'), 3000);
}
function displayColors(colors) {
currentColors = colors;
const gridEl = document.getElementById('colors-grid');
const containerEl = document.getElementById('colors-container');
const emptyStateEl = document.getElementById('empty-state');
const countEl = document.getElementById('colors-count');
const createPaletteBtn = document.getElementById('create-palette-btn');
gridEl.innerHTML = '';
if (colors.length === 0) {
containerEl.classList.add('hidden');
emptyStateEl.classList.remove('hidden');
createPaletteBtn.disabled = true;
return;
}
containerEl.classList.remove('hidden');
emptyStateEl.classList.add('hidden');
createPaletteBtn.disabled = false;
countEl.textContent = `${colors.length} color${colors.length !== 1 ? 's' : ''} found`;
colors.forEach(color => {
const colorItem = document.createElement('div');
colorItem.className = 'flex flex-col items-center gap-2 cursor-pointer transition-transform duration-200 hover:-translate-y-0.5';
colorItem.onclick = () => copyToClipboard(color);
const swatch = document.createElement('div');
swatch.className = 'w-full aspect-square rounded-lg shadow-md border border-black/10';
if (color.length === 9) {
const r = parseInt(color.substring(1, 3), 16);
const g = parseInt(color.substring(3, 5), 16);
const b = parseInt(color.substring(5, 7), 16);
const a = parseInt(color.substring(7, 9), 16) / 255;
swatch.style.backgroundColor = `rgba(${r}, ${g}, ${b}, ${a})`;
} else {
swatch.style.backgroundColor = color;
}
const hex = document.createElement('div');
hex.className = 'text-[10px] font-medium text-gray-600 font-mono break-all text-center';
hex.textContent = color;
colorItem.appendChild(swatch);
colorItem.appendChild(hex);
gridEl.appendChild(colorItem);
});
}
function copyToClipboard(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showStatus(`Copied ${text}`, 'success');
} catch (err) {
showStatus('Failed to copy', 'warning');
}
document.body.removeChild(textarea);
}
function copyAllColors() {
if (currentColors.length === 0) return;
const colorsText = currentColors.join('\\n');
const textarea = document.createElement('textarea');
textarea.value = colorsText;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showStatus('All colors copied!', 'success');
parent.postMessage({ pluginMessage: { type: 'copy-colors' } }, '*');
} catch (err) {
showStatus('Failed to copy', 'warning');
}
document.body.removeChild(textarea);
}
document.getElementById('extract-btn').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'extract-colors' } }, '*');
};
document.getElementById('create-palette-btn').onclick = () => {
if (currentColors.length > 0) {
parent.postMessage({
pluginMessage: {
type: 'create-palette',
colors: currentColors
}
}, '*');
}
};
document.getElementById('copy-btn').onclick = copyAllColors;
onmessage = (event) => {
const msg = event.data.pluginMessage;
if (msg.type === 'colors-extracted') {
displayColors(msg.colors);
showStatus(`Found ${msg.colors.length} unique colors!`, 'success');
}
if (msg.type === 'no-selection') {
showStatus(msg.message, 'warning');
displayColors([]);
}
if (msg.type === 'no-colors') {
showStatus(msg.message, 'info');
displayColors([]);
}
};
</script>
</body>
</html>
Understanding the UI
HTML Structure:
- Header with title and description
- Two action buttons (Extract Colors, Create Palette)
- Status message area
- Color grid container
- Empty state for when no colors are extracted
Tailwind CSS Styling:
Instead of writing hundreds of lines of custom CSS, we use Tailwind’s utility classes:
Benefits:
- 90% less CSS code – From ~200 lines of CSS to ~15 lines (only scrollbar)
- Consistent design – Using Tailwind’s color palette and spacing scale
- Better maintainability – Styling is co-located with HTML
- Smaller bundle – Only the CSS you use gets included
JavaScript Logic:
displayColors()– Renders color swatches dynamically with Tailwind classescopyToClipboard()– Copies individual colors usingexecCommandcopyAllColors()– Copies all colors as newline-separated listshowStatus()– Dynamically applies Tailwind classes for status messages- Message handlers for communication with plugin code
Step 4.3: Configure Network Access for CDN ⚠️ Important
Since we’re using the Tailwind CSS CDN (https://cdn.tailwindcss.com), we must configure network access in manifest.json. Without this, Figma will block the CDN request due to cross-origin restrictions.
Update manifest.json
Add the networkAccess field to your manifest:
{
"name": "Color Palette Extractor",
"id": "123456789",
"api": "1.0.0",
"main": "code.js",
"capabilities": [],
"enableProposedApi": false,
"documentAccess": "dynamic-page",
"editorType": ["figma"],
"ui": "ui.html",
"networkAccess": {
"allowedDomains": ["<https://cdn.tailwindcss.com>"]
}
}
Key Points:
allowedDomains– Array of domains your plugin can access- Required for CDN usage – Without this, Tailwind CSS won’t load
- Security feature – Figma shows allowed domains on your plugin’s Community page
- Multiple domains can be specified:
["<https://cdn.tailwindcss.com>", "<https://api.example.com>". For example, a plugin connected to an AI image checker would need permission to access the service’s API domain before it could analyze visual assets.
Learn more in the Network Access documentation.
Step 5: Build and Test
5.1 Compile TypeScript
Run the build command:
npm run buildThis compiles code.ts into code.js that Figma can execute.
5.2 Load Plugin in Figma
Now let’s test our plugin!
Open Figma Desktop → Plugins → Development → YOUR PLUGIN

OR Open Figma Desktop → Plugins → Development → Import plugin from manifest

5.3 Test the Plugin
Now for the exciting part – let’s see our plugin in action!
- Create some test elements in Figma with different colors and opacity levels
- Select the frame or elements you want to extract colors from
- Run your plugin: Plugins → Development → Color Palette Extractor
- See the results!
Here’s what the plugin looks like in action:
Plugin Interface – Extracted Colors:

The plugin displays all unique colors in a clean grid layout. Notice how it shows both solid colors and colors with opacity using the 8-digit HEXA format (e.g., #E9343499 for 60% opacity).
Individual Color Copy:

Click any color swatch to copy it to your clipboard instantly. Perfect for quick color references!
Generated Color Palette:

Click “Create Palette” to generate a visual palette frame in your Figma canvas. Each color gets its own swatch with the HEX code label, preserving opacity if present.
Next Steps and Enhancements
Now that you have a working plugin, here are some ideas to expand it:
- Export formats – Add support for exporting colors as CSS variables, SCSS, or JSON
- Color sorting – Sort colors by hue, brightness, or frequency
- Gradient support – Extract colors from gradient fills
- Color naming – Integrate with a color naming API
- Style generation – Automatically create Figma color styles
- Contrast checker – Calculate WCAG contrast ratios between colors
- Color harmony – Suggest complementary colors
Advanced: Better Project Structure with Build Tools
The Limitations of Basic Plugins
Our current plugin is simple and works great, but it has one limitation: we can’t organize our code into separate files/folders like you would in React, Next.js, or other modern projects. Everything must be in a single ui.html file.
For basic plugins, this is fine. But for advanced plugins with complex UIs, a proper folder structure becomes essential.
Solution: Use a Bundler
You can use a bundler like esbuild to:
- Organize your code into separate files (
src/ui.html,src/styles.css,src/main.js) - Automatically bundle everything into a single file at build time
- Use modern development practices like hot reload and code splitting
Example: esbuild Build Script
Here’s a build script that inlines CSS and JavaScript into your HTML:
Create build.js:
import esbuild from 'esbuild';
import fs from 'fs';
import path from 'path';
// esbuild configuration
const buildConfig = {
entryPoints: ['src/code.ts'],
bundle: true,
outfile: 'dist/code.js',
platform: 'node',
target: 'es6',
};
// Build function
async function build() {
try {
// Build the main plugin code
await esbuild.build(buildConfig);
// Read the template HTML file
const htmlTemplate = fs.readFileSync(path.join('src', 'ui.html'), 'utf-8');
// Read CSS and JS files
const cssContent = fs.readFileSync(path.join('src', 'styles.css'), 'utf-8');
const jsContent = fs.readFileSync(path.join('src', 'main.js'), 'utf-8');
// Replace external references with inline content
let finalHtml = htmlTemplate
.replace('<link rel="stylesheet" href="styles.css">', `<style>${cssContent}</style>`)
.replace('<script src="main.js"></script>', `<script>${jsContent}</script>`);
// Ensure dist directory exists
const distDir = 'dist';
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// Write the final HTML file
fs.writeFileSync(path.join(distDir, 'ui.html'), finalHtml);
console.log('✅ Plugin built successfully');
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}
build();
Update package.json:
{
"scripts": {
"build": "node build.js",
"watch": "node build.js --watch"
},
"devDependencies": {
"esbuild": "^0.19.0"
}
}
Update package.json:
{
"scripts": {
"build": "node build.js",
"watch": "node build.js --watch"
},
"devDependencies": {
"esbuild": "^0.19.0"
}
}
Folder structure:
my-advanced-plugin/
├── src/
│ ├── code.ts # Main plugin logic
│ ├── ui.html # HTML template with placeholders
│ ├── styles.css # Separate CSS file
│ └── main.js # Separate JavaScript file
├── dist/
│ ├── code.js # Compiled plugin code
│ └── ui.html # Final bundled HTML
├── build.js # Build script
├── manifest.json # Points to dist/code.js and dist/ui.html
└── package.json
Your src/ui.html template:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>My Plugin</h1>
<script src="main.js"></script>
</body>
</html>
At build time, the script will automatically inline the CSS and JavaScript, creating the final dist/ui.html that Figma can use.
Benefits
- ✅ Organized code – Separate files for HTML, CSS, and JavaScript
- ✅ Modern workflow – Use imports, modules, and modern JavaScript features
- ✅ Reusability – Share code between multiple plugin commands
- ✅ Better DX – Hot reload, source maps, and better error messages
- ✅ Production-ready – Minification and optimization
When to Use This
- Basic plugins (like our Color Palette Extractor) → Single
ui.htmlfile is fine - Advanced plugins (complex UIs, multiple views, shared utilities) → Use a bundler
For more information, check out these resources:
Publishing Your Plugin
Once your plugin is ready and tested, you can share it with the Figma community!
Prerequisites for Publishing
Before you can publish, ensure you have:
- Two-factor authentication enabled on your Figma account – Setup guide
- Figma Desktop App – Publishing must be done from the desktop version
- Tested plugin – Make sure everything works as expected
- Plugin assets ready:
- Plugin icon (recommended: 128 x 128px)
- Cover image or video (recommended: 1920 x 1080px)
- Up to 9 additional screenshots/videos
Publishing Steps
1. Open the Manage Plugins Menu
- In Figma Desktop, click the Figma logo → Plugins → Manage plugins
2. Publish Your Plugin
- Click the ⋮ (three dots) next to your plugin → Publish
3. Fill in Plugin Details
- You’ll go through several pages:
Page 1: Describe Your Resource
- Name: Color Palette Extractor
- Tagline: Extract all colors from your designs instantly
- Description: Provide a detailed description of what your plugin does and how to use it
- Category: Choose “Design tools” or relevant category
Page 2: Choose Images
- Upload your plugin icon
- Add a cover image or demo video
- (Optional) Add a playground file where users can try your plugin
- Upload up to 9 additional screenshots showing features
Page 3: Data Security (Optional but recommended)
- Complete the security disclosure form
- Helps users understand your plugin’s data practices
- Review may take up to 2 weeks
Page 4: Final Details
- Choose where to publish (Community or Organization)
- Add a support contact email
- Review network access settings
- (Optional) Add contributors
- (Optional) Enable/disable comments
4. Submit for Review
- Click Publish to submit your plugin. Figma will review it within 5-10 business days.
Review Process
According to Figma’s review guidelines:
- Your plugin will show an “In review” badge during review
- Figma will email you about their decision
- You can still push updates during review
- If approved, you’ll get a “Published” badge
- If rejected, you can address feedback and resubmit
After Publishing
Once approved:
- Your plugin gets its own Community page
- Users can discover and install it
- You’ll get a unique URL:
https://www.figma.com/community/plugin/[id]/[name] - Share your creator profile to showcase all your plugins
Publishing Updates
To publish updates after the initial release:
- Make your code changes
- Run
npm run build - Go to Manage plugins → ⋮ → Publish new version
- Describe what changed in the update
- Submit
Updates are reviewed faster than initial submissions.
Resources and Further Learning
Official Documentation
Community Resources
Source Code
Get the complete source code for this tutorial: GitHub Repository
If you’re looking for a Tailwind Components library, then check out FlyonUI.

Also available in the pro version. It includes
Check it out now!
Conclusion
Congratulations! You’ve just learned how to build a complete Figma plugin from scratch.
Now that you understand the basics, you can:
- Enhance this plugin – Add the features suggested in the “Next Steps” section
- Build your own plugin – What workflow challenge will you solve?
- Join the community – Share your creations and learn from others
- Go deeper – Explore advanced API features like plugin commands, storage, and widgets
Building Figma plugins is an incredibly rewarding way to improve design workflows, learn new technologies, and contribute to the design community. I hope this tutorial has given you the confidence to start building!
What will you create? Share your plugin ideas in the comments below!
Found this helpful?
- ⭐ Star the GitHub repo
- 📢 Share this tutorial with other designers
- 💬 Drop a comment with your questions or plugin ideas
Happy building! 🎨✨



