Building Interactive Figma Widgets

Avatar of Eluda
Eluda on (Updated on )

Figma has always encouraged collaboration between developers and designers. It strives on an endless treasury of community-made plugins. Need 3D elements? There’s a plugin for that. Need abstract SVGs? There’s a plugin for that, too.

That said, the design part of Figma has always been relatively static — always working with unmovable rectangles connected to each other through predefined user interactions. But what if I told you that your designs could suddenly come to life — that they could be animated, interactive, and even stateful? Then, what would separate concept from implementation?

Figma announced in June that it’s bringing JavaScript-powered widgets to the table. Now, designers have can browse and implement logic-driven components straight in Figma!

Say hello to the Widgets API! You want to know what it is and how to use it? That’s exactly what we’re going to do together in this post.

Figma widgets open up tons of possibilities

Imagine that you’re working around the clock with your partner to design a large restaurant application. You’re both already collaborating on the same Figma board; both of you are sharing the exact same document with changes happening on the fly.

Surely, you already know that collaboration involves more that just the design process:

  • project management,
  • hosting polls to gather votes,
  • importing and visualizing mock data,
  • and perhaps even playing a multiplayer game to cool-off after many hours of work.

We just require one person to manage everything and send-out links to other members of the group. But oh, that’s not very efficient, is it?

Well, that’s where widgets come into play. We can conceivably do all of that — yes, everything —without ever leaving Figma.

Here are just a few of the ways you might want to use widgets in Figma:

The list goes on and on. As you can tell, there’s already a plethora of widgets that you can freely use in your documents. In fact, you can add Widgets straight to your board from the Widgets menu (Shift+I).

But we’re not here to learn how to use widgets, because that’s easy. Let us do what we do best: we’re gonna create our own Figma widget! This one will be inspired by Chris Coyier’s design quotes website. We’ll take the API, feed it into the widget, then display random design quotes directly in Figma.

Here’s what we need

I don’t like to be the bearer of bad news, but in order to develop widgets, you must be on Windows or Mac. Linux users, I’m sorry, but you’re out of luck. (You could still use a VM if you want to follow along.)

We’re gonna download the Figma Desktop application. The simplest way to get started is by generating a widget template, straight from the app.

Let’s create a new board by opening the widgets menu (ShiftI), switching to the Development tab, and creating a new item.

Following that, Figma will prompt you to name the new widget and decide whether it’s more tailored towards design boards or FigJam boards too. The former option is sufficient for the purposes of this article.

And the customization doesn’t end here; Figma will also give you the option to start with a pre-made counter widget or an iFrame-enabled alternative that also gives you access to the Canvas and Fetch APIs (as well as all other browser APIs). We’ll go with the simple “Empty” option, but we’ll eventually modify it ourselves to make use of the Fetch API.

You’ll then be prompted to save your new widget project to a special directory in your system. Once that’s done, launch your terminal and direct it to that folder. Don’t run any commands yet — we’ll do that later and purposefully get an error with the goal of learning more about the Widgets API.

Designing the widget

We’re pulling the design straight from Chris Coyier’s design quotes website. So, let’s go there and dive into by firing up DevTools.

The two key shortcuts that I’m using here are Ctrl+Shift+C (or Cmd+Shift+C) to toggle the “Pick element” tool, and Shift+Click to change the color format to HEX code. We’re doing this to learn about the colors, fonts, font weights and font sizes used in Chris’s website. All this information is critical to build a closely-resembling widget in Figma, which will be our next step! You can grab the designed component and use it in your own canvas.

I won’t go into much detail here as this article’s main topic is building widgets by writing code. But I can’t stress enough how important it is to take good care of your widgets’ style… CSS-Tricks already has a plethora of design-oriented Figma tutorials; you won’t regret adding them to your reading list.

Creating the layout for our widget

With design out of the way, it’s time to take our programming fingers out and start building the gears of our widget.

It’s very interesting how Figma translates its design building blocks to React-like components. Frame elements with the auto-layout feature, for example, are represented as the <AutoLayout /> component in code. In addition to that, we’ll be using two more components: <Text /> and <SVG />.

Take a look at my Figma board… I’m precisely asking you to focus on the object tree. It’s what we need to be able to translate our widget design to JSX code.

As you can see, our design quotes widget demands three components to be imported. That’s a decent number of components considering that the full API only contains eight layer-based nodes. But as you’ll soon see, these modules are more than sufficient to craft all kinds of layouts.

// code.tsx
const { widget } = figma;
const { AutoLayout, Text, SVG } = widget;

And with this, we have all we need to go ahead and build the skeleton of our widget like we would in React:

function QuotesWidget() {
  const quote = `...`;
  const author = `...`;

  return (
    <AutoLayout>
      <SVG />
      <AutoLayout>
        <Text>{quote}</Text>
        <Text>— {author}</Text>
      </AutoLayout>
      <SVG />
    </AutoLayout>
  );
}

widget.register(QuotesWidget);

This code is very confusing, to say the least. Right now, we can’t tell the design layers apart. Thankfully, we’re able to easily solve this issue through the use of the name property.

<AutoLayout name={"Quote"}>
  <SVG name={"LeftQuotationMark"} />
  <AutoLayout name={"QuoteContent"}>
    <Text name={"QuoteText"}>{quote}</Text>
    <Text name={"QuoteAuthor"}>— {author}</Text>
  </AutoLayout>
  <SVG name={"RightQuotationMark"} />
</AutoLayout>;

And, of course, we still can’t see our quotation mark SVGs, so let’s work on fixing that. The <SVG/> component accept a srcproperty that takes the source code for an SVG element. There isn’t much to say on this one, so let’s keep it simple and jump straight back to code:

const leftQuotationSvgSrc = `<svg width="117" height="103" viewBox="0 0 117 103" fill="none" xmlns="<http://www.w3.org/2000/svg>">
  // shortened for brevity
</svg>`;
const rightQuotationSvgSrc = `<svg width="118" height="103" viewBox="0 0 118 103" fill="none" xmlns="<http://www.w3.org/2000/svg>">
// shortened for brevity
</svg>`;

function QuotesWidget() {
  return (
    <SVG name={"LeftQuotationMark"} src={leftQuotationSvgSrc} />
    <SVG name={"RightQuotationMark"} src={rightQuotationSvgSrc} />
  );
}

I think we can all agree that everything is much clearer now! When we name things, their purpose suddenly becomes much more obvious to the readers of our code.

Previewing our widget in real-time

Figma offers a great developer experience when building widgets, including (but not limited to ) hot-reloading. With this feature, we’re able to code and preview changes to our widget in real-time.

Get started by opening the widgets menu (Shift+I), switching to the development tab and clicking or dragging your new widget to the board. Unable to locate your widget? Don’t worry, just click on the three-dot menu and import your widget’s manifest.json file. Yes, that’s all it takes bring it back to existence!

Wait, did you get an error message at the bottom of your screen?

If so, let’s investigate. Click on “Open console” and read what it has to say. If the Open console button is gone, there’s an alternative way to open the debugging console. Click on the Figma logo, jump to the widgets category and reveal the development menu.

That error is likely due to the fact that we haven’t compiled our TypeScript to JavaScript yet. We can do that in the command line by running npm install and npm run watch. (or yarn and yarn watch ). No errors this time!

One more obstacle you might hit is that the widget fails to re-render any time the code is changed. We can easily force our widget to update using the following context menu command: Widgets → Re-render widget.

Styling the widget

As it currently stands, the looks of our widgets are still pretty far from our final goal.

So how do we style Figma components from code? Maybe with CSS like we would do in a React project? Negative. With Figma widgets, all the styling happens through a set of well-documented props. Lucky for us, these items are named almost identically to their counterparts in Figma.

We’ll get started by configuring our two <AutoLayout /> components. As you can see in the infographic above, prop names are pretty descriptive of their purpose. This makes it easy for us to jump straight into code and start making some changes. I won’t be showing the whole code again, so please rely on the component names to guide you where the snippets belongs.

<AutoLayout
  name={"Quote"}
  direction={"horizontal"}
  verticalAlignItems={"start"}
  horizontalAlignItems={"center"}
  spacing={54}
  padding={{
    horizontal: 61,
    vertical: 47,
  }}
>
  <AutoLayout
    name={"QuoteContent"}
    direction={"vertical"}
    verticalAlignItems={"end"}
    horizontalAlignItems={"start"}
    spacing={10}
    padding={{
      horizontal: 0,
      vertical: 0,
    }}
  ></AutoLayout>
</AutoLayout>;

We just made a lot of progress! Let’s save and jump back to Figma to see how our widget looks like. Remember how Figma reloads widgets automatically upon new changes?

But it’s not quite there yet. We must also add a background color to the root component:

<AutoLayout name={"Quote"} fill={"#ffffff"}>

Again, take a look at your Figma board and notice how changes can be reflected almost immediately back into the widget.

Let’s move along this guide and style the <Text> components.

After taking a look at the Widgets API documentation, it’s again clear that property names are almost identical to their counterparts in the Figma app, as can be seen in the infographic above. We’ll also be using values from the last section where we inspected Chris’ website.

<Text name={'QuoteText'}
  fontFamily={'Lora'}
  fontSize={36}
  width={700}
  fill={'#545454'}
  fontWeight={'normal'}
>{quote}</Text>

<Text name={'QuoteAuthor'}
  fontFamily={'Raleway'}
  fontSize={26}
  width={700}
  fill={'#16B6DF'}
  fontWeight={'bold'}
  textCase={'upper'}
>— {author}</Text>

Adding state to the widget

Oour widget currently displays the same quote, but we want to pull from the entire pool of quotes at random. We must add state to our widget, which all React developers know is a variable whose change triggers the re-rendering of our component.

With Figma, state is created with the useSyncedState hook; it’s pretty much React’s useState, but it requires programmers to specify a unique key. This requirement stems from the fact that Figma must sync our widget’s state across all clients that may be viewing the same design board, but through different computers.

const { useSyncedState } = widget;

function QuotesWidget() {
  const [quote, setQuote] = useSyncedState("quote-text", "");
  const [author, setAuthor] = useSyncedState("quote-author", "");
}

That’s all the change that we need for now. In the next section, we’ll figure out how to fetch data from the Internet. Spoiler Alert: it’s not as simple as it seems.

Fetching data from the network

Recall when Figma gave us the choice to start with an iFrame-enabled widget. Although we didn’t go with that option, we must still implement some of its features. Let me explain why we can’t simply call fetch() within our widget code.

When you use a widget, you are running JavaScript code on your own computer that’s written by someone else. While all widgets are thoroughly reviewed by the Figma staff, it’s still a huge security hole as we all know how much damage can be created by even one line of JavaScript.

As a result, Figma cannot simply eval() any widget code written by anonymous programmers. Long story short, the team decided that the best solution was running third-party code in a closely-guarded sandbox environment. And as you might have guessed, browser APIs are unavailable in such an environment.

But don’t fret, Figma’s solution to this second problem is <iframe>s. Any HTML code that we write in a file, preferably called ui.html, will have access to all browser APIs. You might be wondering how we can trigger this code from the widget, but we’ll look into that later. Right now, let’s jump back into code:

// manifest.json
{
  "ui": "ui.html"
}
<!-- ui.html -->
<script>
window.onmessage = async (event) => {
  if (event.data.pluginMessage.type === 'networkRequest') {
    // TODO: fetch data from the server

    window.parent.postMessage({
      pluginMessage: {
        // TODO: return fetched data
      }
    }, '*')
  }
}
</script>

That’s the general template for widget-to-iframe communication. Let’s use it to fetch data from the server:

<!-- ui.html -->
<script>
window.onmessage = async (event) => {
  if (event.data.pluginMessage.type === 'networkRequest') {
    // Get random number from 0 to 100
    const randomPage = Math.round(Math.random() * 100)

    // Get a random quote from the Design Quotes API
    const res = await fetch(`https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand&per_page=1&page=${randomPage}&_fields=title,yoast_head_json`)
    const data = await res.json()

    // Extract author name and quote content from response
    const authorName = data[0].title.rendered
    const quoteContent = data[0].yoast_head_json.og_description

    window.parent.postMessage({
      pluginMessage: {
        authorName,
        quoteContent
      }
    }, '*')
  }
}
</script>

We’re leaving out error-handling to keep this simple and to-the-point. Let’s jump back into the widget code and see how we access functions defined in the <iframe>:

function fetchData() {
  return new Promise<void>(resolve => {
    figma.showUI(__html__, {visible: false})
    figma.ui.postMessage({type: 'networkRequest'})

    figma.ui.onmessage = async ({authorName, quoteContent}) => {
      setAuthor(authorName)
      setQuote(quoteContent)

      resolve()
    }
  })
}

As you can see, we’re first telling Figma to expose access to our hidden <iframe> and to trigger an event with the name "networkRequest". We’re handling this event in the ui.html file by checking event.data.pluginMessage.type === 'networkRequest', and then posting data back to the widget.

But nothing is happening yet… We still haven’t called the fetchData() function. If we call it directly in the component function, the following error occurs in the console:

Cannot use showUI during widget rendering.

Figma is telling us not to call showUI directly in the function body… So, where should we put it? The answer to that is one new hook and one new function: useEffect and waitForTask. You might already have familiarity with useEffect if you’re a React developer, but we’re gonna use it here to fetch data from the server when the widget component mounts.

const { useEffect, waitForTask } = widget;

function QuotesWidget() {
  useEffect(() => {
    waitForTask(fetchData());
  });
}

But this will result in yet another “error” where our widget will keep re-rendering with a new quote, forever. This happens because useEffect, by definition, triggers again whenever the widget’s state changes, nay when we call fetchData. And while there’s a technique to only call useEffect once in React, it does not work on Figma’s implementation. From Figma’s docs:

Because of How Widgets Run, useEffect should handle being called multiple times with the same state.

Thankfully, there’s a simple workaround that we can take advantage of and call useEffect only once when the component first mounts, and it’s by checking whether or not the state’s values are still empty:

function QuotesWidget() {
  useEffect(() => {
    if (!author.length & !quote.length) {
      waitForTask(fetchData());
    }
  });
}

You might run into a scary “memory access out of bounds” error. It’s quite common to see in plugin and widget development. Just restart Figma and it won’t be there anymore.

You might have noticed that sometimes, the quote text contains weird characters.

These are Unicode characters and we must properly format them in code:

<!-- ui.html -->
<script>
window.onmessage = async (event) => {
  // ...
  const quoteContent = decodeEntities(data[0].yoast_head_json.og_description);
};

// <https://stackoverflow.com/a/9609450>
var decodeEntities = (function () {
  // this prevents any overhead from creating the object each time
  var element = document.createElement("div");

  function decodeHTMLEntities(str) {
    if (str && typeof str === "string") {
      // strip script/html tags
      str = str.replace(/<script[^>]*>([\\\\S\\\\s]*?)<\\\\/script>/gim, "");
      str = str.replace(/<\\\\/?\\\\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gim, "");
      element.innerHTML = str;
      str = element.textContent;
      element.textContent = "";
    }

    return str;
  }

  return decodeHTMLEntities;
})();
</script>

And voilà, our widget fetched a brand new design quote every single time it’s added to the design board.

Adding a property menu to our widget

While our widget fetches a fresh quote upon instantiation, it would be much more practical if we could do this process again but without deleting it. This section will be short as the solution is quite remarkable. With property menus, we can add interactivity to our widget with a single call to the usePropertyMenu hook.

Credit: Figma Docs.
const { usePropertyMenu } = widget;

function QuotesWidget() {
  usePropertyMenu(
    [
      {
        itemType: "action",
        propertyName: "generate",
	tooltip: "Generate",
        icon: `<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="<http://www.w3.org/2000/svg>">
          <!-- Shortened for brevity -->
        </svg>`,
      },
    ],
    () => fetchData()
  );
}

With one simple hook we’re able to create a button that appears near our widget when it’s selected. That was the last piece that we needed to add in order to complete this project.

Publishing our widget to the public

There’s not much use in building a widget if, well, no one uses it. And while Figma grants organizations with the option to launch private widgets for internal use, it’s much more common to release these little programs to the world.

Figma has a delicate widget review process that may take up 5 to 10 business days. And while the design quotes widget we built together is already in the widget library, I will still demonstrate how it got there. Please don’t attempt to re-publish this widget again as that will only result in removal. But if you gave it some significant alterations, go ahead and share your own widget with the community!

Get started by clicking the widgets menu (Shift+I) and switching to the Development tab to view our widget. Click on the three-dots menu and press Publish.

Figma will prompt you to enter some details about your widget, such as a title, description, and some tags. We’ll also need a 128×128 icon image and a 1920×960 banner image.

After importing all these assets, we still need a screenshot of our widget. Close the publishing modal (don’t worry, you won’t lose your data) and right-click on the widget to reveal an interesting context menu. Find the Copy/Paste ascategory and select Copy as PNG.

With that done, let’s go back to the publishing modal and paste the widget’s screenshot:

Scroll down and finally publish your modal. Celebrate! 🎉

Figma will reach out to you in a couple of days about the status of your modal’s review. In the case of a rejection, you’ll be given the opportunity to make changes and submit again.

Conclusion

We just built a Figma widget from scratch! There are many things not covered here, such as click eventsinput forms, and much more. You can dig into the full source code for the widget in this GitHub repo.

To those who aspire to take their Figma skills to greater levels, I suggest exploring the Widgets community and using what catches your eye as inspiration. Keep building more widgets, keep sharpening your React skills, and before you even realize it, you’ll be teaching me how to do all this.

Further resources

I had to refer to lots of documentation while I was making this widget. I thought I’d share what I found to help the most.

Build more widgets:

Learn widgets in greater depth:

Widgets vs. plugins