Master Figma Plugin Development with JavaScript: A Comprehensive Guide to Writing Your First Figma Plugin and Boosting Your Design Skills

Nick Ciolpan
Graffino
Published in
18 min readJan 27, 2023
https://www.figma.com/community/plugin/1200835054494135844

Are you a Figma fan looking to boost your design skills and streamline your workflow? In this comprehensive guide, we’ll walk you through the process of creating your first Figma plugin using Typescript. With this plugin, you’ll be able to quickly search and insert components from a library file, preview them in real-time, and even update their design as needed. Prerequisites for this tutorial include knowledge of Typescript/Javascript, HTML, and Web APIs. Follow along as we outline the requirements for our plugin and dive into the implementation process, with in-depth insights available in separate entries. By the end of this guide, you’ll have a powerful new tool at your fingertips to help you work more efficiently in Figma.

There are a few prerequisites you should consider when writing your first Figma plugin:

  1. Familiarity with the Figma API: In order to interact with Figma and perform various actions, you'll need to be familiar with the Figma API. You can find the documentation at https://www.figma.com/developers/api.
  2. Basic understanding of web development: Since the plugin will be built using HTML and Typescript/Javascript, it will be helpful to understand web development principles. This includes concepts like HTML tags, CSS styles, and the DOM (Document Object Model).
  3. Ability to use the command line: You'll be using it to create, test, and publish your plugin, so it's important to be comfortable with basic command line commands.
  4. Code editor: You'll need a code editor to write and edit your plugin's code. Some popular options include Visual Studio Code, Atom, and Sublime Text.
  5. Node.js: You'll need to have Node.js installed on your machine in order to build and test your plugin. Node.js is a JavaScript runtime that allows you to run JavaScript on the server side. You can download it from https://nodejs.org/.
  6. Figma account: You'll need a Figma account to test your plugin. If you don't already have one, you can sign up for free at https://www.figma.com/.

Overall, having a strong foundation in Typescript/Javascript, HTML, and web APIs will be the most important prerequisite for writing a Figma plugin. However, being familiar with the other tools and concepts listed above will also be helpful as you work through the process.

I absolutely love Figma as my go-to design tool. It initially gained popularity as a lightweight alternative to Sketch, but it has since surpassed its competitor in both desktop and browser capabilities, as well as seamless collaboration features. At my company, Graffino, we use Figma for all our user interface design needs. Not only is it great for creating beautiful and intuitive UI designs, but it also excels in rapid prototyping. That’s why we decided to create our own plugin to streamline the process even further. It’s a simple concept that surprisingly no one has thought of before.

The goal of the plugin is very simple: we want a file from which we can quickly search for and insert components. To achieve this, we have a few specific requirements in mind:

  1. Quickly open the plugin: We want the plugin to be easily accessible from within Figma.
  2. Immediate type and search: When using the plugin, we want to be able to type and search for components quickly and easily.
  3. Quick Navigation: We want the plugin to be easy to navigate, with a clear and intuitive interface.
  4. Immediate preview: We want to be able to preview components in real time as we search for them.
  5. Quick insert: Once we’ve found the desired component, we want to be able to insert it into our design quickly.

There are also a few optional features we’d like to include:

  1. Change source: We’d like to be able to switch between different library files within the plugin.
  2. Deactivate preview: We’d like to have the option to turn off the preview feature if desired. (soon)
  3. Update design: We’d like to be able to make adjustments to the design of the inserted component directly within the plugin. (soon)

We’ll be mapping out these requirements and going through the process of implementing each of them in more detail. In-depth bits will be available in separate entries, so be sure to check those out as well. “

The Figma plugin is composed of only two files: a HTML file that dictates the user interface, and a TypeScript file that acts as a controller, providing logic handling and data sourcing. These two files communicate through an event system. In other words, the plugin’s functionality is divided between the visual appearance and the underlying logic. The files are named “ui.html” and “code.ts” respectively.

src 
|- code.ts
|- ui.html

Our plugin will be more complex than just executing a single action. Therefore, we have chosen to use a template that includes both a user interface (UI) file and a logic file. Additionally, it comes with an example to help us better understand its functionality and navigate through it. This will be useful to us as we will not be starting from scratch. The basic template includes a “Insert” button, a “Cancel” button, and a “Hello” text.

Spitting out the two aforementioned files:

figma.showUI(__html__);

figma.ui.onmessage = msg => {
if (msg.type === 'create-rectangles') {
const nodes: SceneNode[] = [];
for (let i = 0; i < msg.count; 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();
};

and:

<h2>Rectangle Creator</h2>
<p>Count: <input id="count" value="5"></p>
<button id="create">Create</button>
<button id="cancel">Cancel</button>
<script>

document.getElementById('create').onclick = () => {
const textbox = document.getElementById('count');
const count = parseInt(textbox.value, 10);
parent.postMessage({ pluginMessage: { type: 'create-rectangles', count } }, '*')
}

document.getElementById('cancel').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}

</script>

When building a Figma plugin, one of the most important aspects is understanding how to use the Figma API to interact with the design document. Two key parts of this process are the use of the figma.showUI() function and the creation of a UI file.

The figma.showUI() function is used to display a user interface for your plugin within the Figma app. It takes a single argument, which is the HTML for your UI file. In the example provided, the UI file is passed as a string with the variable __html__. This function displays the UI, allowing the user to interact with it and execute actions within the plugin.

The second key part of building a Figma plugin is the creation of a UI file. This file defines the visual appearance of the plugin’s user interface and is often written in HTML and JavaScript. The example provided includes a UI file with a simple layout that has a “Rectangle Creator” heading, a text input for counting rectangles and “Create” and “Cancel” buttons. The JavaScript code in the UI file is used to handle user interactions with the UI, such as clicking the “Create” or “Cancel” buttons. In this example, when the “Create” button is clicked, the plugin sends a message to the Figma API with the type “create-rectangles” and the count of rectangles to be created. On the other hand, when the “Cancel” button is clicked, the plugin sends a message with the type “cancel”.

The other important file is the logic file that is written in TypeScript. It uses the figma.ui.onmessage event to handle the messages sent by the UI file and perform actions accordingly. In this example, when the message "create-rectangles" is received, the plugin creates the specified number of rectangles, sets their appearance and position, and adds them to the current page. It also selects the newly created rectangles and scrolls the viewport to them. After performing the desired action, the plugin closes itself.

Let’s delve a little deeper into the philosophy behind the inner workings of Figma plugins.

In this diagram, the User activates the plugin, which causes Figma to load the plugin’s JavaScript code, execute it, and perform actions on the design document. The plugin’s JavaScript code then uses the figma.showUI() function to display a user interface (UI) within the Figma app. The user interacts with the UI, for example by clicking a button, inputting a value, etc. The UI code uses the parent.postMessage() function to send a message to the plugin's JavaScript code, indicating the type of action that the user has requested. The plugin's JavaScript code uses the figma.ui.onmessage event to handle the message and perform the corresponding action. The plugin's JavaScript code uses the Figma API to update the design document according to the user's request. Finally, the plugin's JavaScript code uses the figma.closePlugin() function to close the plugin when the user is done interacting with it.

Let’s hope that this will become clearer as we construct a practical example and demonstrate the steps I took in creating my plugin.

I developed this plugin to streamline my process of fast prototyping and user experience (UX) exploration. As a product designer, I often work with clients to brainstorm and create numerous ideas and screens for products in high-innovation spaces. This process can be time-consuming as I often find me copying and pasting components from various sources or designing them on the spot, which takes away from the main objective of the activity: quickly assembling screens that showcase information architecture and user journeys.

My plugin addresses this problem by providing an efficient solution to the above-mentioned process. It allows me to quickly prototype and validate use cases and user journeys, saving time and staying focused on the main goal. It’s an essential tool for product designers that work on high-innovation projects and want to fast prototype and explore different user journeys.

At its core, the plugin offers the following functionality: when you have one or multiple pages containing component toolkits in the same Figma file, you can use the plugin’s shortcut key to open a simple search interface. Here, you can type in the name of the component you’re looking for, and the plugin will use fuzzy search to provide you with a list of the closest results, with the most relevant one selected. For example, if you’re searching for a dropdown menu, you can enter ‘dropd..’, and the plugin will quickly populate a list of all dropdown-related components, with the most relevant one at the top. This can save you time and effort, as it eliminates the need to manually search through the file and bring the component you’re looking for into focus.

The first step would be to create the user interface (UI) for the dropdown, which will be used to render the dropdown, capture user events, and send them via the message bus to the logic handler.

Please don’t mind the inline styles.

<div>
<div
style="display: flex; width: 100%; font-size: 16px; line-height: 1; border-bottom: 1px solid #4C2FFC">
<input
id="search-component-widget"
type="search"
placeholder="I'm looking for a..."
style="outline: none; width: 100%; height: 30px; border: 0; font-size: inherit; line-height: inherit;"
>
<div style="display:flex; align-items: center; font-size: inherit; line-height: inherit;"><span style="font-family: Arial, Helvetica, sans-serif;">from: </span>
<select
style="min-width: 100px; width: 100%; height: 30px; border: 0; font-size: inherit; line-height: inherit;"
id="source-page-widget"></select></div>
</div>
<ul
id="component-results-outlet"
style="list-style: none; padding: 0; margin-top: 10px;"
><ul/>
</div>

Let’s begin the coding process. In its most basic form, we want to be able to send a search query and receive a list of all the components that match the criteria. To achieve this, we’ll break down the process into two parts:

First, we’ll focus on the UI.html file:

  • Create a search input that will allow the user to send a query
  • Create a space to display the results once they are received from the “backend”

This will form the foundation for the search functionality and allow us to easily display the results to the user. It is important to be mindful of the inline styles that we’ll use to ensure that the UI looks good and that the search input and results are easily distinguishable.

In addition, the UI file also includes a significant amount of JavaScript code to handle the data-rich events returned by the code.ts file, and make the plugin function properly.

  const getQuery = () => document.getElementById('search-component-widget').value;
const getSource = () => document.getElementById('source-page-widget').value;

const applyListItemHoverStyles = (li) => {
li.style.backgroundColor = '#4C2FFC';
li.style.color= 'white';
li.style.padding = '3px';
}

const applyListItemStyles = (li) => {
li.style.backgroundColor = 'transparent';
li.style.color= 'black';
li.style.padding = '5px';
li.style.borderRadius= '5px';
li.style.cursor = "pointer";
li.style.fontSize = "inherit";
li.style.lineHeight = "inherit";
li.style.fontFamily = "Arial, Helvetica, sans-serif";
}

parent.postMessage({ pluginMessage: { type: 'fetch-pages'} }, '*')
parent.postMessage({ pluginMessage: { type: 'get-current-source'} }, '*')

window.onmessage = ({data}) => {
((searchComponentWidget) => {
if (!!searchComponentWidget) searchComponentWidget.focus();
})(document.getElementById('search-component-widget'));

switch (data.pluginMessage.type) {
case 'retrieved-current-source':
document.getElementById('source-page-widget').value = data.pluginMessage.source;
break;

case 'pages':
((source) => {
if (!!source) {
const { pages } = data.pluginMessage;
pages.forEach(page => {
const option = document.createElement('option');
option.value = page.title;
option.textContent = page.title;
source.appendChild(option);
});
}
})(document.getElementById('source-page-widget'))
break;

case 'results':
((outlet) => {
outlet.innerHTML = '';
const fragment = new DocumentFragment()

const { results }= data.pluginMessage;

results.map((result, index) => {
const li = document.createElement('li');
li.textContent = result
li.classList.add('result');
applyListItemStyles(li);

if (index === 0) {
parent.postMessage({ pluginMessage: { type: 'preview', componentName: li.textContent, source: getSource()} }, '*')
li.classList.add('selected');
applyListItemHoverStyles(li);
}
fragment.appendChild(li);
})

outlet.appendChild(fragment);
})(document.getElementById('component-results-outlet'));
break;

default:
break;
}
}

document.body.addEventListener("click", function (event) {
if (event.target.classList.contains("result")) {
parent.postMessage({ pluginMessage: { type: 'preview:insert' } }, '*')
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}
});

document.body.addEventListener("keydown", function (event) {
const results = document.querySelectorAll('.result');
if (results.length > 0) {
if (event.key === 'ArrowDown') {
const selected = document.querySelector('.selected');
if (selected) {
selected.classList.remove('selected');
applyListItemStyles(selected);
const next = selected.nextElementSibling;
if (next) {
next.classList.add('selected');
applyListItemHoverStyles(next);
parent.postMessage({ pluginMessage: { type: 'preview', componentName: next.textContent, source: getSource()} }, '*');
} else {
results[0].classList.add('selected');
applyListItemHoverStyles(results[0]);
parent.postMessage({ pluginMessage: { type: 'preview', componentName: results[0].textContent, source: getSource()} }, '*');
}
}
} else if (event.key === 'ArrowUp') {
const selected = document.querySelector('.selected');
if (selected) {
selected.classList.remove('selected');
applyListItemStyles(selected);
const prev = selected.previousElementSibling;
if (prev) {
prev.classList.add('selected');
applyListItemHoverStyles(prev);
parent.postMessage({ pluginMessage: { type: 'preview', componentName: prev.textContent, source: getSource()} }, '*');
} else {
results[results.length - 1].classList.add('selected');
applyListItemHoverStyles(results[results.length - 1]);
parent.postMessage({ pluginMessage: { type: 'preview', componentName: results[results.length - 1].textContent, source: getSource()} }, '*');
}
}
} else if (event.key === 'Enter') {
const selected = document.querySelector('.selected');
if (selected) {
parent.postMessage({ pluginMessage: { type: 'preview:insert' } }, '*')
}
}
}
if (event.key === 'Escape') {
parent.postMessage({ pluginMessage: { type: 'preview:remove' } }, '*')
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}
});

document.body.addEventListener("mouseover", (event) => {
if (event.target.classList.contains("result")) {
Array.from(document.querySelectorAll('.result')).map(result => {
result.classList.remove('selected');
applyListItemStyles(result);
});
event.target.classList.add('selected');
applyListItemHoverStyles(event.target);
parent.postMessage({ pluginMessage: { type: 'preview', componentName: event.target.textContent, source: getSource()} }, '*');
}
});

document.getElementById('search-component-widget').oninput = () => {
parent.postMessage({ pluginMessage: { type: 'fetch-results', componentName: getQuery(), source: getSource()} }, '*')
}
document.getElementById('source-page-widget').onchange = (event) => {
parent.postMessage({ pluginMessage: { type: 'update-source', source: event.target.value, componentName: getQuery() } }, '*')
}

For your understanding and ease of reference, the code provided above is the entire JavaScript found in the ui.html file. Now, let’s examine each section and explain its purpose and functionality in detail.

To make it easier for us to proceed, I want to point out that we have created a few utility functions for convenient access to the DOM and for applying styles to frequently used elements. These are outlined below.

  const getQuery = () => document.getElementById('search-component-widget').value;
const getSource = () => document.getElementById('source-page-widget').value;

// styling utils
const applyListItemHoverStyles = (li) => {
li.style.backgroundColor = '#4C2FFC';
li.style.color= 'white';
li.style.padding = '3px';
}

const applyListItemStyles = (li) => {
li.style.backgroundColor = 'transparent';
li.style.color= 'black';
li.style.padding = '5px';
li.style.borderRadius= '5px';
li.style.cursor = "pointer";
li.style.fontSize = "inherit";
li.style.lineHeight = "inherit";
li.style.fontFamily = "Arial, Helvetica, sans-serif";
}

The function retrieves data from a specified Figma page or multiple Figma pages (as a feature option), filters the results by a given name, and returns an array containing the filtered results.

On initialization, the plugin sends messages to obtain the names of all available pages in the Figma file. These names are then made available in a drop-down options menu. The plugin also sets up a listener for messages from the “backend” and subsequently handles each received message based on its type.

parent.postMessage({ pluginMessage: { type: 'fetch-pages'} }, '*')
parent.postMessage({ pluginMessage: { type: 'get-current-source'} }, '*')

window.onmessage = ({data}) => {
((searchComponentWidget) => {
if (!!searchComponentWidget) searchComponentWidget.focus();
})(document.getElementById('search-component-widget'));

switch (data.pluginMessage.type) {
......
case 'retrieved-current-source':
document.getElementById('source-page-widget').value = data.pluginMessage.source;
break;

The initial case directs the frontend to identify and retrieve new components from the designated source.

case 'pages':
((source) => {
if (!!source) {
const { pages } = data.pluginMessage;
pages.forEach(page => {
const option = document.createElement('option');
option.value = page.title;
option.textContent = page.title;
source.appendChild(option);
});
}
})(document.getElementById('source-page-widget'))
break;

The second case fills the source page drop-down menu with all pages that are present in the current Figma file.

        case 'results':
((outlet) => {
outlet.innerHTML = '';
const fragment = new DocumentFragment()

const { results }= data.pluginMessage;

results.map((result, index) => {
const li = document.createElement('li');
li.textContent = result
li.classList.add('result');
applyListItemStyles(li);

if (index === 0) {
parent.postMessage({ pluginMessage: { type: 'preview', componentName: li.textContent, source: getSource()} }, '*')
li.classList.add('selected');
applyListItemHoverStyles(li);
}
fragment.appendChild(li);
})

outlet.appendChild(fragment);
})(document.getElementById('component-results-outlet'));
break;

The final case obtains the identifiers of all matching search results, creates the corresponding list items, applies the pre-defined styles, and attaches them to the designated area in the HTML code.

The remaining code facilitates easy navigation of the search and drop-down fields and also handles the styling and interactions for hover and other user interactions.

  document.body.addEventListener("keydown", function (event) {
const results = document.querySelectorAll('.result');
if (results.length > 0) {
if (event.key === 'ArrowDown') {
const selected = document.querySelector('.selected');
...

Next, we will examine the “backend” code.ts file. As before, let’s divide the entire file before diving into the details of its functionality.

function* walkTree(node: any) {
yield node;
let children = node.children;
if (children) {
for (let child of children) {
yield* walkTree(child)
}
}
}

let latestPreview = null;

const useWalker = (source) => {
const sourcePage = figma.root.children.find(page => page.name === source)
let walker = walkTree(sourcePage)
return {walker};
}
const findAll = (query: string, source: any, level = null) => {
if (query.length <= 1) return [];

const { walker } = useWalker(source);
const nodes = [];
let res: { value: any; };

const isMatch = (name) => {
return name.toLowerCase().includes(query.toLowerCase())
}

while (!(res = walker.next()).done) {
let node = res.value
if (node.type === (level ?? 'INSTANCE') && isMatch(node.name)) {
nodes.push(node);
}
}

return nodes;
}

const findOne = (match: any, source: any, level = null) => {
const { walker } = useWalker(source);
let res: { value: any; };
let result: any;

while (!(res = walker.next()).done) {
let node = res.value
if (node.type === (level ?? 'INSTANCE') && node.name === match) {
result = node;
}
}

return result;
}

const generateResults = (msg) => {
const results = findAll(msg.componentName, msg.source, msg.level);
figma.ui.postMessage({
type: 'results',
results: Array.from(new Map(results.map(r => [r.name, r])).keys())
});
}

const generatePreview = (msg) => {
const node = findOne(msg.componentName, msg.source, msg.level)
if (!!latestPreview) {
try {
latestPreview.remove();
} catch (e) {
latestPreview = null;
}
}

if (!!node) {
node.visible = true;
latestPreview = node.clone();
figma.currentPage.selection = [latestPreview];
figma.viewport.scrollAndZoomIntoView([latestPreview]);

}
}

figma.showUI(__html__);
figma.ui.resize(300, 300);

figma.ui.onmessage = async msg => {
if (msg.type === 'fetch-pages') {
const pages = figma.root.children;
figma.ui.postMessage({type: 'pages',figma: figma, pages: pages.map(r => ({title: r.name, id: r.id}))});
}

if (msg.type === 'get-current-source') {
figma.ui.postMessage({
type: 'retrieved-current-source',
source: await figma.clientStorage.getAsync('source')
});
}

if (msg.type === 'update-source') {
try {
latestPreview.remove();
} catch (e) {
latestPreview = null;
}

latestPreview = null;
await figma.clientStorage.setAsync('source', msg.source);
const results = findAll(msg.componentName, msg.source, msg.level);

if (results.length > 0) {
generatePreview(msg);
generateResults(msg);
} else {
generateResults(msg);
}
}

if (msg.type === 'fetch-results') {
generateResults(msg);
}

if (msg.type === 'preview:remove') {
if (!!latestPreview) {
try {
latestPreview.remove();
} catch (e) {
latestPreview = null;
}
}

latestPreview = null;
}

if (msg.type === 'preview:insert') {
if (!!latestPreview) {
latestPreview = null;
figma.closePlugin();
}
}

if (msg.type === 'preview') {
generatePreview(msg);
}

if (msg.type === 'insert-component') {
const node = findOne(msg.componentName, msg.source, msg.level)
node.visible = true;
const clone = node.clone();

figma.currentPage.selection = [clone];
figma.viewport.scrollAndZoomIntoView([clone]);
}

if (msg.type === 'cancel') {
figma.closePlugin();
}
};

The Figma document model is made up of various nested components, connected through grouping. The “walker” generator function is designed to traverse the document tree, starting from the root and moving through the branches, until it reaches the node with the desired name.

function* walkTree(node: any) {
yield node;
let children = node.children;
if (children) {
for (let child of children) {
yield* walkTree(child)
}
}
}

const useWalker = (source) => {
const sourcePage = figma.root.children.find(page => page.name === source)
let walker = walkTree(sourcePage)
return {walker};
}

The “findOne” and “findAll” functions serve the purpose of allowing us to retrieve either the first matching instance closest to our search query or all the matching instances with varying levels of precision. The initial version of the code, as presented above, has not undergone significant refactoring. Now, let’s take this opportunity to refactor the code to better highlight the similarity in functionality and emphasize the main task: returning one or multiple match instances.


const isMatch = (name, query) => {
return name.toLowerCase().includes(query.toLowerCase())
}

const findNode = (source, level = null, match = null) => {
const { walker } = useWalker(source);
const nodes = [];
let res;

while (!(res = walker.next()).done) {
let node = res.value
if (node.type === (level ?? 'INSTANCE')) {
if (match) {
if (node.name === match) {
return node;
}
} else if (isMatch(node.name, match)) {
nodes.push(node);
}
}
}

return match ? null : nodes;
}

const findAll = (query, source, level = null) => {
if (query.length <= 1) return [];
return findNode(source, level, null, query);
}

const findOne = (match, source, level = null) => {
return findNode(source, level, match);
}

We are initializing the layout for the plugin’s interface. This layout corresponds to the window that opens in Figma when the plugin is activated.

figma.showUI(__html__);
figma.ui.resize(300, 300);

Afterward, we begin listening for incoming messages broadcasted by the UI component.

figma.ui.onmessage = async msg => {
...

I will organize the messages based on their purpose, and also list the related helper functions next to them. The initial set of messages deal with making all pages accessible in the dropdown menu and keeping track of the current source for Figma to reference when searching for the target file. Currently, this is limited to searching within a specific page, but expanding the search to include the entire Figma file is a feature that is being considered for the near future.

  if (msg.type === 'fetch-pages') {
const pages = figma.root.children;
figma.ui.postMessage({type: 'pages',figma: figma, pages: pages.map(r => ({title: r.name, id: r.id}))});
}

if (msg.type === 'get-current-source') {
figma.ui.postMessage({
type: 'retrieved-current-source',
source: await figma.clientStorage.getAsync('source')
});
}

if (msg.type === 'update-source') {
try {
latestPreview.remove();
} catch (e) {
latestPreview = null;
}

latestPreview = null;
await figma.clientStorage.setAsync('source', msg.source);
const results = findAll(msg.componentName, msg.source, msg.level);

if (results.length > 0) {
generatePreview(msg);
generateResults(msg);
} else {
generateResults(msg);
}
}

Once a decision has been made, the results are generated and the process of inserting the selected component into the Figma page begins. The component is also focused, allowing for immediate editing. This is achieved by creating a copy of the target component, and positioning it at the cursor’s location.

  if (msg.type === 'fetch-results') {
generateResults(msg);
}

if (msg.type === 'insert-component') {
const node = findOne(msg.componentName, msg.source, msg.level)
node.visible = true;
const clone = node.clone();

figma.currentPage.selection = [clone];
figma.viewport.scrollAndZoomIntoView([clone]);
}
const generateResults = (msg) => {
const results = findAll(msg.componentName, msg.source, msg.level);
figma.ui.postMessage({
type: 'results',
results: Array.from(new Map(results.map(r => [r.name, r])).keys())
});
}

However, it may be difficult to determine if the currently selected component is the correct one. Many designers do not name their components accurately or have multiple similarly named components. To address this issue, a real-time preview is generated and displayed on the page as the user scrolls through the results. This is accomplished by temporarily inserting and removing the component in real-time as the user navigates through the results. When the user makes a final selection, the component is cloned, inserted and focused, as previously described.

  if (msg.type === 'preview:remove') {
if (!!latestPreview) {
try {
latestPreview.remove();
} catch (e) {
latestPreview = null;
}
}

latestPreview = null;
}

if (msg.type === 'preview:insert') {
// The node is already insered at this point
// We just need to close the plugin
if (!!latestPreview) {
latestPreview = null;
figma.closePlugin();
}
}

if (msg.type === 'preview') {
generatePreview(msg);
}
const generatePreview = (msg) => {
const node = findOne(msg.componentName, msg.source, msg.level)
if (!!latestPreview) {
try {
latestPreview.remove();
} catch (e) {
latestPreview = null;
}
}

if (!!node) {
node.visible = true;
latestPreview = node.clone();
figma.currentPage.selection = [latestPreview];
figma.viewport.scrollAndZoomIntoView([latestPreview]);

}
}

It is important to close the plugin when you are finished using it. Failure to do so will result in the plugin continuing to run, which will display a cancel button at the bottom of the screen.


if (msg.type === 'cancel') {
figma.closePlugin();
}

Overall, developing a Figma plugin is not overly difficult and can be a fun and rewarding experience. Creating a tool that can greatly improve one’s workflow is a valuable asset and not something that every product designer is able to do. If you are an extensive Figma user and have some programming knowledge, I would encourage you to try creating your own tools, whether for professional use, as a hobby or as a quick solution for immediate needs. The key is to understand the messaging bridges between the UI and code files, limitations of the Figma document and how the Figma document API is organized. The documentation provided by Figma is also very helpful in this regard. For example, https://www.figma.com/plugin-docs/api/properties/nodes-parent/#docsNav.

Heres’s the end result in action:

You can find it here: https://www.figma.com/community/plugin/1200835054494135844

With that being said, I wish you good luck and fun in making your own plugins.

--

--

Nick Ciolpan
Graffino

Co-founder of Graffino. I have extensive experience as a full stack developer, managing clients and building state of the art platforms.