How to Build Figma Plugins Easily: A Complete Guide

Written By Ajay Patel 23 mins
How to Build Figma Plugins Easily: A Complete Guide

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.

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:

  1. Solve Real Problems – If you’ve encountered a workflow challenge, chances are others have too
  2. 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.  
  3. Give Back to the Community – Share your solution with thousands of designers worldwide
  4. Career Growth – Showcase your problem-solving abilities and technical skills
  5. 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:

1.1 Create a plugin in Figma

Open the Figma Desktop app and follow these steps:

Plugins → Development → New Plugin…

create pluging in figma step 1

1.2 Configure Plugin Details

Give your plugin a descriptive name: “Color Palette Extractor.”

configure plugin

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
create plugin

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.

plugin created

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 details
  • code.ts – Your main plugin code that runs in Figma’s sandbox
  • ui.html – Your plugin’s user interface (UI context)
  • tsconfig.json – TypeScript compiler settings
  • package.json – Node.js dependencies and build scripts

Step 2: Install Dependencies

Open your terminal in the plugin directory and install dependencies:

npm install

This installs:

  • @figma/plugin-typings – TypeScript type definitions for the Figma Plugin API
  • typescript – 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 classes
  • copyToClipboard() – Copies individual colors using execCommand
  • copyAllColors() – Copies all colors as newline-separated list
  • showStatus() – 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  build

This 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

load plugin in figma

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

load plugin in figma alternate way

5.3 Test the Plugin

Now for the exciting part – let’s see our plugin in action!

  1. Create some test elements in Figma with different colors and opacity levels
  2. Select the frame or elements you want to extract colors from
  3. Run your plugin: Plugins → Development → Color Palette Extractor
  4. See the results!

Here’s what the plugin looks like in action:

Plugin Interface – Extracted Colors:

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:

Individual Color Copy

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

Generated Color Palette:

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:

  1. Export formats – Add support for exporting colors as CSS variables, SCSS, or JSON
  2. Color sorting – Sort colors by hue, brightness, or frequency
  3. Gradient support – Extract colors from gradient fills
  4. Color naming – Integrate with a color naming API
  5. Style generation – Automatically create Figma color styles
  6. Contrast checker – Calculate WCAG contrast ratios between colors
  7. 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:

  1. Organize your code into separate files (src/ui.htmlsrc/styles.csssrc/main.js)
  2. Automatically bundle everything into a single file at build time
  3. 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.html file 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:

  1. Two-factor authentication enabled on your Figma account – Setup guide
  2. Figma Desktop App – Publishing must be done from the desktop version
  3. Tested plugin – Make sure everything works as expected
  4. 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:

  1. Make your code changes
  2. Run npm run build
  3. Go to Manage plugins →  → Publish new version
  4. Describe what changed in the update
  5. 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.

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:

  1. Enhance this plugin – Add the features suggested in the “Next Steps” section
  2. Build your own plugin – What workflow challenge will you solve?
  3. Join the community – Share your creations and learn from others
  4. Go deeper – Explore advanced API features like plugin commandsstorage, 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! 🎨✨

Ajay Patel

CEO & Co-Founder

With over 15 years in the tech industry, I’ve honed my skills as an entrepreneur, programmer, and innovator. As the co-founder of Clevision, I've created ThemeSelection, PixInvent, FlyonUI & ShadcnStudio.

Get notified about upcoming Premium & Free themes, Unique promo codes and Sales 🎉 !

] Announcements
Announcements Banner