DEV Community

Cover image for How We Built a Figma Plugin in Just 300 Lines of Code (Step-by-Step Guide)
Olushola O.
Olushola O.

Posted on • Edited on

How We Built a Figma Plugin in Just 300 Lines of Code (Step-by-Step Guide)

Figma plugins are a set of programs or applications that adds a cherry on top of Figma functionalities. Figma plugins are usually created by the community of developers. It helps to improve product designers overall experience and enhance time management. You can interact fully with the Figma boards and also modify as much as you need to.
To find out more, check the Figma plugin documentation.

Why was this Figma plugin developed?

A few months back, My friend Sam who is also a professional product designer needed a way to search for layers and texts on Figma boards, then he reached out to me as a developer and friend to build one :).

Before this moment, I had no previous knowledge of how Figma plugins are developed, hence the meme below.
A picture of us talking about creating a plugin

Challenges encountered

  • What programming stack is used to develop Figma plugins
  • What name should we give the Figma plugin?
  • How do we finally publish the Figma plugin?

Figma plugins are developed using HTML and JavaScript only. You have the option of using TypeScript(recommended) and also styling your HTML elements using CSS for a better and more intuitive user interface.

To get the most out of this article, you need a basic understanding of TypeScript and HTML

To start with, you need Node.js installed on your computer. You can test the installation by displaying the version of Node.js on your computer via the command line or terminal.



node --version


Enter fullscreen mode Exit fullscreen mode

We will use NPM as our package manager. You can use yarn if you prefer. Do note that NPM comes with the Node.Js installation.

Install TypeScript on your machine
Install TypeScript globally so you can run it anywhere, from your favourite code editors to your terminal.



npm install -g typescript


Enter fullscreen mode Exit fullscreen mode

Get the Figma desktop application
To test the plugin, you need to download the desktop application. Figma needs to parse your codebase and run it locally for you to test it out on your Figma boards.
Download the Figma desktop application on their website.

If you already have the desktop application, it is recommended to upgrade to the latest version

Create a new plugin

Login to the Desktop application and open the file editor, create a new document or open an existing one.

Next, Go to the menu, then navigate to Plugins > Development > New plugin as depicted below:

Create new figma plugin menu

A modal titled "Create plugin" will pop open. Give it a name, then choose "Figma design" and click "Next". In the next screen, you will select "With UI & browser APIs" and then click "save as", this will prompt you to save the plugin anywhere on disk.

Selecting Figma design on Figma desktop

Selecting UI & browser API on Figma desktop

Please note that these steps might differ depending on your Figma desktop version.

Setup the project and run the sample plugin

Open the folder created using Visual Studio Code(recommended), you can decide to use your favourite code editor.

Install the Figma plugin typings package using NPM



npm install --save-dev @figma/plugin-typings


Enter fullscreen mode Exit fullscreen mode

Next, start the typescript compilation using the terminal.

Run npm run build -- --watch

Make sure your terminal is in the directory of the Figma plugin

To test the plugin, switch to the Figma desktop app, go to the menu then navigate to Plugin > Development > "Name of your plugin"

Subsequently, you can navigate to Plugin > "Run Last Plugin" if you have run the plugin previously.

Once you run the plugin you should see a popup modal showing you the sample plugin and you can interact with it.

Select your plugin from the dropdown
The default Figma sample plugin

Let's Build FigFinder

Yes, we named the plugin FigFinder and I will explain how it was developed in this article.

FigFinder helps to search for a layer or text on the Figma board, it receives a search query and layer type via a form, and the form data is sent to the Figma API which in turn sends back a list of components that matches the search query. The list is displayed on a result page. When each item on the list is clicked, an event is sent to the Figma API which in turn navigates to where the component can be found on the Figma board.

This Plugin allows designers to easily search for a component by name, even if there are thousands of components on the Figma board.

Building the plugin interface

Open ui.html then empty the file, and create a new div and script element respectively. The div element will contain our plugin interface while the script element will contain the business logic.
Create two new div elements inside the div element and give them an id named "search-page" and "result-page" respectively.



<div>
  <div id="search-page" style="padding: 24px;">
  </div>
  <div id="result-page" class="hide" style="padding: 0px 24px">
  </div>
</div>


Enter fullscreen mode Exit fullscreen mode

Give the div with an id of "result-page" a class of "hide", we will use this class to toggle the visibility of the search page and result page.

Next, paste the code below inside the "search-page" div element



<div style="margin-bottom: 24px;">
      <label for="search">Keyword</label>
      <div class="input-icons">
        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-1">
          <path
            d="M7.66659 14.0002C11.1644 14.0002 13.9999 11.1646 13.9999 7.66683C13.9999 4.16903 11.1644 1.3335 7.66659 1.3335C4.16878 1.3335 1.33325 4.16903 1.33325 7.66683C1.33325 11.1646 4.16878 14.0002 7.66659 14.0002Z"
            stroke="#999999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
          <path d="M14.6666 14.6668L13.3333 13.3335" stroke="#999999" stroke-width="1.5" stroke-linecap="round"
            stroke-linejoin="round" />
        </svg>
        <input id="search" type="text" class="text-field" placeholder="Search" />
      </div>
    </div>

    <div style="margin-bottom: 16px;">
      <label for="layer-type">Layer type</label>
      <select class="select-field" id="layer-type">
        <option value="text">Text</option>
        <option value="frame">Frame</option>
      </select>
    </div>

    <div class="info-box">
      <svg width="36" height="36" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
          d="M7.99992 14.6668C11.6666 14.6668 14.6666 11.6668 14.6666 8.00016C14.6666 4.3335 11.6666 1.3335 7.99992 1.3335C4.33325 1.3335 1.33325 4.3335 1.33325 8.00016C1.33325 11.6668 4.33325 14.6668 7.99992 14.6668Z"
          stroke="#474747" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
        <path d="M8 5.3335V8.66683" stroke="#474747" stroke-width="1.5" stroke-linecap="round"
          stroke-linejoin="round" />
        <path d="M7.99634 10.6665H8.00233" stroke="#292D32" stroke-width="2" stroke-linecap="round"
          stroke-linejoin="round" />
      </svg>

      <p>Input your keyword phrase into the input “Keyword” and select your layer type. Click on the search button to
        run plugin.👌</p>
    </div>

    <button type="button" class="search-btn" id="search-btn">Search</button>



Enter fullscreen mode Exit fullscreen mode
  • We added the input field that obtains the user search query used to filter and find components from the Figma API
  • Dropdown input field that allows users to search for either a text or a layer from the Figma board.
  • Button element used to trigger a function to send the form data to the Figma API.

Next, we populate the div element of the "result-page"



<div class="back-button" id="back-button">
      <svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M6.38016 3.95312L2.3335 7.99979L6.38016 12.0465" stroke="#7D7D7D" stroke-width="1.5"
          stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
        <path d="M13.6668 8H2.44678" stroke="#7D7D7D" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"
          stroke-linejoin="round" />
      </svg>
      <p style="margin: 0;">Back to search</p>
    </div>
    <div id="empty-result" style="padding: 48px 24px; text-align:center;" class="hide">
      <svg width="110" height="102" viewBox="0 0 110 102" fill="none" xmlns="http://www.w3.org/2000/svg">
        <circle cx="49.6812" cy="49.6812" r="49.6812" fill="#F1F1F1" />
        <circle cx="49.275" cy="49.7657" r="28.7081" fill="#F1F1F1" stroke="#C4C4C4" stroke-width="2" />
        <circle cx="49.2748" cy="49.766" r="20.2201" fill="#FFFEFF" stroke="#C4C4C4" stroke-width="2" />
        <path
          d="M49.2749 34.1653C49.2749 33.8537 49.0221 33.6 48.7106 33.6109C44.8118 33.747 41.0862 35.2895 38.2287 37.964C35.3712 40.6385 33.5858 44.254 33.1923 48.1353C33.1609 48.4453 33.3973 48.7143 33.7082 48.7349C34.0191 48.7555 34.2868 48.5199 34.319 48.21C34.693 44.6145 36.3514 41.2667 38.9998 38.7879C41.6482 36.3091 45.0983 34.8756 48.7107 34.7401C49.0221 34.7284 49.2749 34.4769 49.2749 34.1653Z"
          fill="#C4C4C4" />
        <rect x="71.4574" y="81.2695" width="10.3062" height="29.6873" rx="5.15309"
          transform="rotate(-45 71.4574 81.2695)" fill="#F1F1F1" stroke="#C4C4C4" stroke-width="2" />
        <rect x="82.1211" y="83.6025" width="1.34862" height="11.3078" rx="0.674309"
          transform="rotate(-45 82.1211 83.6025)" fill="white" />
        <rect x="70.9087" y="74.4121" width="1.38436" height="5.53746" transform="rotate(-45 70.9087 74.4121)"
          fill="#C4C4C4" />
        <rect x="85.5972" y="46.2798" width="13.8436" height="1.38436" rx="0.692182" fill="#C4C4C4" />
        <rect y="57.7319" width="13.8436" height="1.38436" rx="0.692182" fill="#C4C4C4" />
        <rect x="102.209" y="46.2798" width="6.92182" height="1.38436" rx="0.692182" fill="#C4C4C4" />
        <rect x="89.0581" y="51.3589" width="6.92182" height="1.38436" rx="0.692182" fill="#C4C4C4" />
        <rect x="3.46094" y="62.811" width="6.92182" height="1.38436" rx="0.692182" fill="#C4C4C4" />
      </svg>
      <p style="color: #7D7D7D; margin:16px;">
        We could not find any result that
        matches the keyword “search”
      </p>
    </div>
    <div id="result" class="hide">

    </div>


Enter fullscreen mode Exit fullscreen mode
  • The result section has a placeholder for displaying a simple message when no result is available.
  • It also has a div element with an id of "result" which is empty by default and will get populated when the Figma API returns a list of available search results.

Next, we have to style the interface. Inside the ui.html but outside the primary div element, create a style element which will enclose all our styles.



<style>
  * {
    font-family: Arial, Helvetica, sans-serif;
  }

  #result div {
    display: flex;
    align-items: center;
    cursor: pointer;
    margin-bottom: 12px;
    background: #FAFAFA;
    border-radius: 4px;
    padding: 12px 16px;
    height: 20px;
  }

  #result div.active {
    background-color: #D6D8DB;
  }

  #result div p {
    margin: 2px 12px;
  }

  .hide {
    display: none;
  }
  .text-field {
    width: 100%;
    border-radius: 6px;
    padding: 16px 12px 16px 32px;
    border: 0.5 solid grey;
  }
  .select-field {
    width: 100%;
    border-radius: 6px;
    padding: 16px;
    margin: 10px 0px;
    border: 0.5 solid grey;
  }

  .input-icons {
    width: 100%;
    margin: 10px 0px;
  }

  .input-icons svg {
    position: absolute;
  }

  .input-icons svg {
    position: absolute;
  }

  .icon-1 {
    padding: 16px 0px 0px 12px;
  }

  .info-box {
    margin-top: 24px;
    margin-bottom: 28px;
    padding: 8px 16px 8px 16px;
    background-color: #F5F5F5;
    display: flex;
    flex-direction: row;
    align-items: center;
    border-radius: 4px;
  }

  .back-button {
    display: flex;
    flex-direction: row;
    align-items: center;
    cursor: pointer;
    width: fit-content;
    column-gap: 12px;
    margin: 20px 0 0 0;
  }

  .back-button p {
    color: #7D7D7D;
    margin-left: 12px;
  }

  .info-box p {
    font-size: 13px;
    margin-left: 16px;
    color: #555555;
  }

  .search-btn {
    height: 52px;
    background-color: #1C77C3;
    width: 100%;
    border-radius: 4px;
    font-size: 16px;
    border: none;
    color: white;
  }
</style>


Enter fullscreen mode Exit fullscreen mode

We styled the search page, buttons, and result page elements.

The styling gives the Figma plugin its overall blazing look 🔥🔥

The interface of this Figma plugin was actually designed on Figma by Sam.

Run the Figma plugin and see how it looks. It should look like what we have below.

FigFinder plugin

Writing the plugin functionality

Next, we open the code.ts file. We will write all the business logic for the Figma plugin in this file.

We use the Figma API to create an interface that scaffolds how the elements will be displayed figma.showUI(__html__, { title: 'FigFinder', height: 460, width: 490 });, next we create a listener that gets a message from the ui.html then we run a set of instructions based on the message received.



figma.showUI(__html__, { title: 'FigFinder', height: 460, width: 490 });

figma.ui.onmessage = msg => {

};



Enter fullscreen mode Exit fullscreen mode

Inside the onmessage listener, we create two conditional statements, one to process the search query and one to search for the selected element on the Figma board.



  if (msg.type === 'process-search') {

    const { searchQuery, layerType } = msg.query;
    if (layerType == 'all') {
      console.log(searchQuery)

      const node = figma.currentPage.findAll(node =>
        node.name.toLowerCase().includes(searchQuery.toLowerCase()));
      console.log(node.map(n => n.name))
      const result = node.map(n => n.name);
      figma.ui.postMessage({ count: result.length, type: 'all', result })
    } else if (layerType == 'text') {
      console.log(searchQuery)
      const node = figma.currentPage.findAll(node => node.type === "TEXT"
        &&
        node.characters.toLowerCase().includes(searchQuery.toLowerCase())) as TextNode[];
      console.log(node.map(n => n.characters))
      const result = node.map(n => n.characters);
      figma.ui.postMessage({ count: result.length, type: 'text', result })
    } else if (layerType == 'frame') {
      console.log(searchQuery)
      const node = figma.currentPage.findAll(node => node.type === "FRAME"
        &&
        node.name.toLowerCase().includes(searchQuery.toLowerCase())) as TextNode[];
      console.log(node.map(n => n.name))
      const result = node.map(n => n.name);
      figma.ui.postMessage({ count: result.length, type: 'frame', result })
    } else {
      figma.ui.postMessage({ count: 0, type: layerType, result: [] })
    }
  } else if (msg.type === 'search-item') {
    const { searchQuery, layerType } = msg.query;
    let nodesFound ;
    if (layerType == 'text') {
      nodesFound = figma.currentPage.findChildren((node) => node.type === 'TEXT' && node.characters.toLowerCase().includes(searchQuery.toLowerCase()));
    }
    if (layerType == 'frame') {
      nodesFound = figma.currentPage.findChildren((node) => node.type === 'FRAME' && node.name.toLowerCase().includes(searchQuery.toLowerCase()));
    }

    if (nodesFound) {
    figma.currentPage.selection = nodesFound;
    figma.viewport.scrollAndZoomIntoView(nodesFound);
    }

  } else {
    figma.closePlugin('Command not available');
  }



Enter fullscreen mode Exit fullscreen mode

We used figma.currentPage to interact with the current Figma board while figma.postMessage sends an event to ui.html.

Make everything work together

Next, we update the script element inside ui.html to listen to the message coming from code.ts. Inside this listener, we process the messages received and then display a list of elements based on the information.



 var layer_type = '';

  onmessage = (event) => {
    const pluginMessageResult = event.data.pluginMessage;
    if (pluginMessageResult.count && pluginMessageResult.count > 0) {
      const resultDiv = document.getElementById('result');
      resultDiv.classList.remove('hide');
      layer_type = pluginMessageResult.type;
      resultDiv.innerHTML = `<p>Search results: ${pluginMessageResult.count} item(s)</p>`
      for (let index = 0; index < pluginMessageResult.count; index++) {
        const element = pluginMessageResult.result[index];
        const parentDivNode = document.createElement("div");


        const node = document.createElement("p");
        const textnode = document.createTextNode(element);
        node.appendChild(textnode);

        parentDivNode.setAttribute('content', element)
        parentDivNode.onclick = handleClick;

        parentDivNode.appendChild(node)

        resultDiv.appendChild(parentDivNode)

        node.insertAdjacentHTML("beforebegin", getIcon());

      }
    } else {
      document.getElementById('empty-result').classList.remove('hide');
    }
    console.log("got this from the plugin code", event.data.pluginMessage)
  }

  const textSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.78003 4.77986V3.56652C1.78003 2.79986 2.40003 2.18652 3.16003 2.18652H12.84C13.6067 2.18652 14.22 2.80652 14.22 3.56652V4.77986" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.8136V2.74023" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.37329 13.8135H10.6266" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;

  const frameSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="4.5" y1="1.3335" x2="4.5" y2="14.6668" stroke="black"/>
<line x1="14.6667" y1="5.1665" x2="1.33341" y2="5.1665" stroke="black"/>
<line x1="14.6667" y1="11.1665" x2="1.33341" y2="11.1665" stroke="black"/>
<line x1="11.8333" y1="1.3335" x2="11.8333" y2="14.6668" stroke="black"/>
</svg>
`;

  function getIcon() {
    if (layer_type == 'text') {
      return textSvg;
    }
    return frameSvg;
  }



  function handleClick() {
    const query = { searchQuery: this.getAttribute('content'), layerType: layer_type };
    parent.postMessage({ pluginMessage: { type: 'search-item', query } }, '*')

    const resultDiv = document.getElementById('result');
    const divElement = resultDiv.querySelector('div.active');
    if (divElement) {
      divElement.classList.remove('active');
    }
    this.classList.add('active')
  }


Enter fullscreen mode Exit fullscreen mode

The handleClick function sends an event to code.ts so it can search for the selected element on the Figma board.

Next, we handle the search logic, which will send an event to code.ts to get elements that match the search query based on the user input.

We add a click event on the button with the id "search-btn" then parent.postMessage is used to send the form data to the code.ts. This data sent will be processed inside the onmessage listener in code.ts



 document.getElementById('search-btn').onclick = (event) => {

    const inputFieldText = document.getElementById('search').value;
    const selectFieldText = document.getElementById('layer-type').value;
    console.log(inputFieldText, selectFieldText)

    const query = { searchQuery: inputFieldText, layerType: selectFieldText };
    parent.postMessage({ pluginMessage: { type: 'process-search', query } }, '*')

    document.getElementById('search-page').classList.add('hide');
    document.getElementById('result-page').classList.remove('hide');
  }


Enter fullscreen mode Exit fullscreen mode

Finally, we add the logic to close the result page when the user returns to the search page.



document.getElementById('back-button').onclick = () => {
document.getElementById('search-page').classList.remove('hide');
document.getElementById('result').innerHTML = '';
document.getElementById('result-page').classList.add('hide');
document.getElementById('empty-result').classList.add('hide');
}

Enter fullscreen mode Exit fullscreen mode




Test the Figma plugin

Run the Figma plugin and interact with it. You can type in the input field to search for a frame or text. Then, finally, click the search button. You will get a list of components on the next page. You can click on each item of the result that sends an event to the Figma API to navigate to the particular Figma component on the Figma board.

If the response returns an empty list, the Figma plugin alerts the user that no results are available.

A demo of the plugin

The Figma plugin was built with little over 300 lines of code when combining the ui.html and code.ts files.

What I learned from this project

  • I put my critical thinking to the test as there were little or no resources on building Figma plugins except the documentation and the Figma community.
  • I was able to refresh my knowledge of vanilla JavaScript.
  • I was able to build a solid foundation for creating complex Figma plugins in the future.

Current limitations for developing a Figma plugin

Creating Figma plugins is exciting but has a few drawbacks that make it less fun. I discussed some of them below:

  • Using external assets does not seem to work, so external CSS, images, and JavaScript are not allowed at the time of writing this article
  • Using JavaScript frameworks for building Figma plugins is impossible currently, so you have to be well-equipped to write Vanilla JavaScript. You can use CDN for importing libraries like jQuery though I have not personally tested it.
  • Lastly, you are confined to the project structure and cannot explore or set up your preferred architecture.

If you decide to build a Figma plugin and publish it, please refer to their official website, where they explained the steps to get the Figma plugin deployed and used by the Figma creators' community.

Conclusion

This article explained what a Figma plugin is and its usage. I also explained how I built FigFinder(A Figma plugin that finds components on the Figma board). I cannot wait to see what you will develop with it. You can also reach out to me on Twitter.
The complete codebase of this Figma plugin is available on GitHub. Please do well to give a GitHub star to the repository.

If you found this helpful, leave a reaction ❤️

Top comments (0)