Design

Figma DIY: Building a color system plugin

If you’re interested in translating or adapting this post, please.

Learn how to build a missing Figma feature with just some basic knowledge of HTML, CSS, and JavaScript. In this step-by-step tutorial, we draw up a simple mock interface, discover a better way of working with colors and create a small plugin that makes it possible. If you are working at the intersection of design and web development—this post is for you.

Before we jump into coding, we will make a case for a missing Figma feature that could change the way we manage colors in a growing design project. Once we think up a dynamic color system—we will implement it by extending the feature set of Figma with our simple plugin.

If you have developed for the web before—working on a Figma plugin will make sense right away. If your experience with HTML, CSS, and JavaScript is limited—we still encourage you to try, as we broke the process down into simple steps with explanations and code examples that you can copy and paste into your editor.

We hope it will whet your appetite for development, and you will think of your own plugins to build in the future. We will also walk you through the preparation step to set up your development environment from scratch.

To give you some motivation: here’s the video of our little plugin in action.

a simple-color-system plugin you will have developed by the end of this tutorial

Ready for the design-code adventure? Then tune in!

Those hexed colors

Check out another Figma post from our design department: Auto Layout: Practical tips for dynamic designs

As experienced designers, we want to limit ourselves to a clearly defined palette that contains just enough distinctive colors to be reused between different elements of our interface. For this little example, we’ve chosen six colors in total: black, white, blue, pink, green, and light gray.

So, how do we assign these colors when we start a new project from an empty slate? In Figma, we select the shape we want to fill and use the color picker in the “Fill” menu to select the desired shade, or we type in the hex-code directly. 028CFC for our blue.

Assigning color to the element

Selecting the hex-code for the color. Blue is #028CFC

When we are set on a color, we might want to reuse it. As you can see, in our example, several elements share the same shade of blue: the color of the “Personal” tag, checkmarks for selected tags, the background of the primary “Apply” button, and the label of the secondary “Cancel” button.

How did we achieve that? We copied the hex value of the fill from one shape, selected other shapes, and pasted the value to trigger color change.

That is a valid approach to get yourself off the ground, but it does not scale well. As you create more components in the same design system, you will end up copying and pasting more color values.

If a decision is taken to change the shade of blue across the entire interface, you will find yourself in a copy-paste hell.

Color with style

How do we avoid a Sisyphian task of changing colors manually? Luckily, Figma has a concept of styles.

You can create a paint style based on a color fill by clicking on a “painted” element and selecting Fill->Styles->Create Style.

Creating a paint style

Creating a paint style with a blue fill

We can quickly convert all the six of our existing colors into paint styles by giving them human-readable names like “Blue” or “Light Gray”.

There’s no need to argue that as human beings, we will have an easier time with adjectives than with hexadecimal numbers for reasoning about colors.

Take a look at the “Tag Selector Color Styles” frame in our sample project.

Paint styles named as fill colors

Paint styles named as fill colors

No more copy-paste! We see all our color styles at a glance inside the “Design” pane. We can also edit any style globally. As we change the color property inside the “Edit Style” menu—all the elements that depend on a selected style will change their color. Try it! It’s magic!

Style and function

OK, wait a minute. We currently have just one shade of blue in our design. Still, it serves two distinct functions: it is the color of the “Personal” tag, but also the general accent color of our interface that is shared between the checkmarks, the primary button’s background, and the secondary button’s label.

We might not want to change the tag’s color if we want to tweak the general accent. Having all blue elements styled as “Blue” will make our color system too rigid.

The alternative will be to name styles not based on how they look but on what function they serve in our UI.

Third time’s the charm, so let’s take a final go at our styles. Inside the “Tag Selector Functional Styles” frame in our sample project, we have named the styles according to their function and assigned them to respective elements. All the black text is now styled “Text primary”, tag colors are styled with tag names, and the blue is now “Accent UI”.

Paint styles named for their function

Paint styles named for their function

Dynamic color system

Our little Figma project now has eleven paint styles in total. Some are named based on the underlying color fill (“Pink”, “Green”), and some are named according to the function they serve (“Text Primary”, “Tag Personal”).

In essence, all of them are nothing but an abstraction over hex-codes for colors. When we apply “Blue”, “Tag Personal”, or “Accent UI” to some element, we give it the same color fill: 028CFC. That said, all our styles depend on the same source.

Here lies the problem that will make our project hard to maintain in the future: if we need to change the common underlying fill for several styles, we are back to square one: copying and pasting hex values.

There’s just one crucial step missing from turning our convention into a flexible, dynamic color system: relationships between styles.

What if we could make our styles depend on each other?

That way, we could base our “Blue” style on a certain hex color, then use “Blue” as the source for both “Tag Personal” and “Accent UI”.

If we draw this on a napkin, it will look something like this:

Linked color styles

Linked color styles

If we want to change the shade of blue globally, we just need to edit the source for “Blue”. It will affect “Tag Personal” and “Accent UI”, and both styles will automatically get the new shade.

In other words, a change in any underlying style should start a chain reaction, making our interface conform to new rules.

If we want to change the accent color to a shade of green—no problem, just make it depend on a “Green” style:

Changing a source style for a functional style makes your designs flexible

Now the color of a personal tag and the dominant color of the interface can go their separate ways.

Now that seems like a powerful and scalable color system for any project. There is just one problem: Figma wouldn’t let us control styles that way.

At least, not while we stick to the features that come out of the box.

Building a Figma plugin from scratch

As Figma is based on web technologies, nothing stops us from developing our own features. All we need is some basic knowledge of JavaScript, CSS, and HTML. We will learn to operate Figma’s paint styles programmatically and make the popular design software do our bidding. Figma always works in a browser (the desktop app is based on Chrome), so we can use the standard browser APIs to create the interface that we want.

We will take you through the whole process of building a plugin. It is better if you find time to code along, so you can see the final functionality shaping up step by step.

Figma provides a great setup guide that we recommend you follow so we can start on the same page. Follow the official instructions to install:

  • Visual Studio Code
  • Node and npm
  • TypeScript

Note that you need to download Figma desktop to work with your plugin in development. A web version will not be able to access your code locally.

In Figma desktop’s menu, choose Plugins -> Development -> New Plugin. Give it the name simple-color-system and choose the template called “With UI & browser APIs”.

Steps to follow to create a new plugin

Steps to follow to create a new plugin

Choose a place where you want to save your plugin folder. Now open that folder from your Visual Studio Code.

You should see a bunch of boilerplate files inside the “Explorer” pane, but the only ones we are interested in are code.ts and ui.html. At the start, they contain some sample code. No worries, we will replace it with our own in no time.

There are just a few more steps we need to take to finish setting up our development environment.

  • Open the terminal inside your Visual Studio Code. In the top menu: Terminal -> New Terminal.

Paste the line below into the prompt and hit “Enter”:

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

Make sure you install Figma plugin typings for your project

Make sure you install Figma plugin typings for your project

Figma allows you to write TypeScript, the advanced superset of JavaScript, that you don’t have to learn to follow this tutorial: the standard JS will do just fine. The command above, however, is necessary, so Figma can automatically translate .ts files to .js.

  • Select Terminal -> Run Build Task in the top menu of your Visual Studio Code or hit Cmd-Shift-B (Ctrl-Shift-B on Windows) to bring up the build task interface. Select “tsc: watch - tsconfig.json” and hit “Enter”.

Now Figma will transpile your TypeScript code into JavaScript on the fly

Now Figma will transpile your TypeScript code into JavaScript on the fly

Note that these two are very important steps: without it, you won’t be able to test your plugin code in real-time.

Reading paint styles

Let’s see now how we can use Figma’s plugin API methods to read all the paint styles in our project. Delete all the code from the code.ts file and replace it with this:

// code.ts

figma.showUI(__html__);

const styles = figma.getLocalPaintStyles();
const styleNames = styles.map((style) => style.name);
console.log(styleNames);

Here we are using Figma’s built-in getLocalPaintStyles method to fetch an array of PaintStyle objects, each containing information about an existing paint style in our project.

If we look up the documentation for PaintStyle, we will see all its properties, but the one we are currently interested in is name.

JavaScript’s map can be a tricky concept to learn if you are just beginning to code. There are plenty of accessible resources that can help you master it.

Then we use JavaScript’s map method for transforming arrays to read only the name property of each element. That will create a new array of names that we save to a styleNames variable. Finally, we print that array to the console.

Now, delete all the sample code from ui.html and leave only an empty <script> tag inside:

<!-- ui.html -->
<script></script>

We are ready to launch our plugin for the first time. It does not have any UI yet. All it can do for now is log the names of paint styles to a developer console. We use console.log as a simple debugging tool to check if our setup is complete and the code works as intended.

In your Figma’s top menu, choose Plugins -> Development -> Open Console. To launch your plugin, choose Plugins -> Development -> simple-color-system.

If all goes well, you should see the names of all our styles in the logs of the console. If you don’t see anything or see the error messages in your console—make sure you followed the plugin developer’s setup guide to the letter.

A smoke test for our plugin

A smoke test for our plugin

Now it’s time to see how we can display those names in the interface of our plugin. For that, we will need the code inside our code.ts to send a message to the ui.html, which is the interface of our plugin defined as any other HTML page: with markup tags, styles, and scripts.

Here’s how we do it from the sending side:

// code.ts

figma.showUI(__html__);

const styles = figma.getLocalPaintStyles();
const styleNames = styles.map((style) => style.name);

// Sending a message to ui.html
figma.ui.postMessage({
  type: "render",
  styleNames,
});

The contents of the message is an object that contains a payload (array of style names) and has a type property. When we receive messages, we can select how we react to them based on their type.

On the receiving side, inside our ui.html, we need to create an empty HTML list element, give it a unique ID, then write some JavaScript code inside the <script> tag.

The code will handle the message sent from code.ts. We will use JavaScript’s built-in reduce method to transform an array of names into a string of HTML that will contain items for our list tag. Then we will use the DOM API to replace the contents of the list tag with our string of HTML.

Open your ui.html file and replace it with this code:

<!-- ui.html -->
<ul id="styles-list"></ul>
<script>
  // Select the ul tag
  const stylesListElement = document.getElementById("styles-list");

  // Handle incoming message from code.ts
  onmessage = ({ data: { pluginMessage } }) => {
    const messageType = pluginMessage.type;
    // React only to "render" message
    if (messageType === "render") {
      // Get an array of style names from the message
      const styleNames = pluginMessage.styleNames;
      // Turn array of names into a single HTML string with li tags
      const rowHTML = styleNames.reduce(
        (resultString, styleName) => resultString + `<li>${styleName}</li>`,
        ""
      );
      // Insert li tags inside the ul
      stylesListElement.innerHTML = rowHTML;
    }
  };
</script>

Now you can come back to Figma and run your plugin again. You can either select Plugins -> Development -> simple-color-system from the top menu or just press Cmd-Alt-P (Ctrl-Alt-P on Windows). You will see your plugin window pop-up, showing all your project’s paint styles as a standard HTML bulleted list.

A first shot at the plugin interface

A first shot at the plugin interface

Linking paint styles

Our plugin now has a rudimentary interface, but it still doesn’t do much. As you remember, we want to create relationships between paint styles by assigning one style as a source for another. How can we express it in our interface? We can show a pair of styles for each row: one as a source and another as a receiver.

Style assignment, visualized

Style assignment, visualized

Now it is immediately clear that the name of the receiver style is on the left, and the source is on the right. We used the equals character to convey the idea of “assigning” styles to one another like we assign variables in code.

And how are we going to express that the style has a source when we create it in Figma?

If you click on “Edit style” for any of the styles in a document, you will see that you can change the name, the underlying color, and a description. The description is just an arbitrary text field. It looks like we can use it to store information about the source. Edit your “Accent UI” style to give it a description “Blue”.

Editing a receiver style to provide information on source style

Editing a receiver style to provide information on source style

Now we need to manipulate those descriptions from our plugin and change style properties based on our convention.

Getting a description out of a PaintStyle object is easy. We can use the description property just as we used the name property to get a style’s name.

Now we need to do more work in our code.ts file: instead of gathering all existing style names and sending them over to the interface, we have to:

  • For each style, try to find the corresponding “source” style by reading the style description.
  • For each row of our interface, send an object of the form:
{
  receiver: {
    name: "Accent UI",
  },
  source: {
    name: "Blue",
  }
}

We can use this object later in the interface to put the receiving style name on the left of each “equation” and the source style name on the right.

To help us achieve the desired result, we will define a couple of helper functions: getSourceForStyle and getReceiverSourcePairs. Here are the new contents of the code.ts:

// code.ts

// Helpers

// Look at all styles' descriptions to find a matching source style and return if found
function getSourceForStyle(style, styles) {
  for (const currentStyle of styles) {
    if (style.description === currentStyle.name) return currentStyle;
  }
  return null;
}

// Form pairs
function getReceiverSourcePairs(styles) {
  return styles.map((style) => ({
    receiver: style,
    source: getSourceForStyle(style, styles),
  }));
}

// Main plugin logic
figma.showUI(__html__);

const styles = figma.getLocalPaintStyles();
// Call the helper function to form pairs
const receiverSourcePairs = getReceiverSourcePairs(styles);

// Transform data into the shape we want before sending it to the UI
const receiverSourceData = receiverSourcePairs.map((pair) => ({
  receiver: {
    name: pair.receiver.name,
  },
  source: {
    // Some styles don't have a source, so we need to check
    name: pair.source ? pair.source.name : "",
  },
}));

// Debug
console.log(receiverSourceData);

// Send to UI
figma.ui.postMessage({
  type: "render",
  receiverSourceData,
});

It may feel like a lot is going on in this code, but there’s no reason to get overwhelmed. We use helper functions defined at the top of the file to go over an array of all styles in our project and compare their names and descriptions.

If we have a style called “Text Primary” with a description “Black”, and there exists a style with the name “Black” among other styles—we know that “Text Primary” is a receiver, and “Black” is a source. We put their names together in an object with receiver and source keys and send it over to the interface.

Before you run this code, let’s create the links between the styles in our Figma project. Put “Black” into the description of a “Text Primary” style, “Pink” into “Tag Work”, “Green” into “Tag Trips” and “Blue” into both “Tag Personal” and “Accent UI”.

Now, open the developer’s console in Figma and run the plugin!

Logging style pairs: receiver and source

Logging style pairs: receiver and source

You can see that our UI doesn’t display anything, and there is an error in the console, but that’s expectable: we haven’t yet taught our ui.html to handle a new message.

What is important that we can log the source for every style by reading the style’s description.

Now it’s time to take care of the interface. We want a new markup for each row: a name of the style and the input for its source next to it.

As the new row HTML is becoming quite evolved, we will create it in a separate function.

<!-- ui.html -->
<ul id="styles-list"></ul>

<script>
  function markupRow(receiverSourceData) {
    return `<li>
    <span>${receiverSourceData.receiver.name}</span>
    <span> = </span>
    <input
      type="text"
      placeholder="None"
      value="${receiverSourceData.source.name}"
    >
  </li>`;
  }

  const stylesListElement = document.getElementById("styles-list");

  onmessage = ({ data: { pluginMessage } }) => {
    const messageType = pluginMessage.type;

    if (messageType === "render") {
      const stylesNames = pluginMessage.receiverSourceData;
      const rowHTML = pluginMessage.receiverSourceData.reduce(
        (resultString, pair) => resultString + markupRow(pair),
        ""
      );
      stylesListElement.innerHTML = rowHTML;
    }
  };
</script>

If you run your plugin now, this is what you will be able to see:

Displaying relationships between styles in the plugin interface

Displaying relationships between styles in the plugin interface

We can correctly read and display relationships between the styles in our project. We cannot yet use our plugin to change the styles according to new sources, but we are almost there.

Reassigning paint styles

Now we want our plugin to react to user input: when we change the name of the source style in a field, it should be able to repaint the parent style accordingly.

To repaint a style, all we need from our plugin’s perspective is to replace its paints property:

// Don't paste this code anywhere yet
receiverStyle.paints = sourceStyle.paints;

To do that, we need to find a receiver style first. Figma API does not give us a method to find a style based on just a name. We have to know its id property: a unique string identifier that every style in a project already has. Then we’ll be able to use figma.getStyleById method to select a PaintStyle object and replace its paints.

That means that we need to send a receiver style ID along with the name when we send a list of styles as a message from code.ts to ui.html. Let’s update the part of code.ts that forms a payload for the message to add another property to our receiver object.

// code.ts
// ...

const receiverSourceData = receiverSourcePairs.map((pair) => ({
  receiver: {
    name: pair.receiver.name,
    id: pair.receiver.id, // New!
  },
  source: {
    name: pair.source ? pair.source.name : "",
  },
}));

// ...

Now let’s update the markupRow helper inside ui.html to store the receiver style ID inside a custom HTML attribute data-receiver-id. We will need it soon when we learn how to send messages back from ui.html to code.ts.

function markupRow(receiverSourceData) {
  return `<li>
    <span>${receiverSourceData.receiver.name}</span>
    <span> = </span>
    <input
      type="text"
      placeholder="None"
      value="${receiverSourceData.source.name}"
+     data-receiver-id="${receiverSourceData.receiver.id}"
    >
  </li>`;
}

Now we need to teach our ui.html to react to user changes inside row inputs. As they are standard HTML inputs, we can use the good old event listener from the DOM API and call any method when the input content has changed.

Here’s the fully updated ui.html:

<!-- ui.html -->
<ul id="styles-list"></ul>

<script>
  function markupRow(receiverSourceData) {
    return `<li>
    <span>${receiverSourceData.receiver.name}</span>
    <span> = </span>
    <input
      type="text"
      placeholder="None"
      value="${receiverSourceData.source.name}"
      data-receiver-id="${receiverSourceData.receiver.id}"
    >
  </li>`;
  }

  // New!
  function handleTextFieldChange(event) {
    console.log(
      `Receiver ID: ${event.target.dataset.receiverId} | New Source name: ${event.target.value}`
    );
  }

  const stylesListElement = document.getElementById("styles-list");
  // New!
  stylesListElement.addEventListener("change", handleTextFieldChange);

  onmessage = ({ data: { pluginMessage } }) => {
    const messageType = pluginMessage.type;

    if (messageType === "render") {
      const stylesNames = pluginMessage.receiverSourceData;
      const rowHTML = pluginMessage.receiverSourceData.reduce(
        (resultString, pair) => resultString + markupRow(pair),
        ""
      );
      stylesListElement.innerHTML = rowHTML;
    }
  };
</script>

You can now run the plugin, change the source style of “Accent UI” from “Blue” to “Pink”, hit “Enter” and examine the developer console. You should see something like this:

Receiver ID: S:9c376fc6968a696508c4efb1632a2d5266e49468, | New Source name: Pink

That is our even listener getting triggered. It reads the ID of the receiver style from the HTML attribute (value of data-receiver-id can be retrieved from JS with target.dataset.receiverId) and gets the new source name from the input with target.data.value.

Great, now we need to get that information back to code.ts so we can find both styles and reassign the paints property. As you remember, the modules of our plugin can communicate through messages. That is how we sent a message from code.ts to ui.html:

// From code.ts to ui.html

figma.ui.postMessage({
  type: "render",
  receiverSourceData,
});

Sending the message in the opposite direction is similar, with the only difference that from inside ui.html we don’t have direct access to the figma.ui object. Instead, you can access the parent object that also has the postMessage method, but the syntax is slightly different:

// From ui.html to code.ts

parent.postMessage(
  {
    pluginMessage: {
      type: "my-message",
      myData,
    },
  },
  "*"
);

Note that the message must be wrapped into a pluginMessage object and there is an additional string argument to the call that always has the value "*". You can read more about this quirk in the Figma documentation. With this knowledge, we can change the event listener’s handler in our ui.html to pack the ID of a receiver and the new name of the source into a message:

<!-- ui.html -->
<script>
  // Only change this function and leave the rest of the code unchanged:
  function handleTextFieldChange(event) {
    parent.postMessage(
      {
        pluginMessage: {
          type: "update-source",
          newSourceName: event.target.value,
          receiverId: event.target.dataset.receiverId,
        },
      },
      "*"
    );
  }
</script>

Now, let’s react to this message inside our code.ts. Add this code to the very bottom of the file:

// code.ts

// ...rest of the code above

figma.ui.onmessage = (msg) => {
  if (msg.type == "update-source") {
    const receiverStyle = figma.getStyleById(msg.receiverId) as PaintStyle;
    // We can only find the source style by name
    const sourceStyle = styles.find(
      (style) => style.name === msg.newSourceName
    );
    // return early if one of the styles is not found
    if (!(receiverStyle && sourceStyle)) return;

    // Replace description and properties for the receiver
    receiverStyle.description = msg.newSourceName;
    receiverStyle.paints = sourceStyle.paints;

    // Show a success message in the Figma interface
    figma.notify(`New source style: ${msg.newSourceName}`);
  }
};

If you never had any experience with TypeScript, you might be wondering what as PaintStyle means. That is called a type cast: figma.getStyleById returns the elements of type BaseStyle, but we explicitly need a PaintStyle to get to the paints property. You can read more about TypeScript features in Figma documentation. The best thing about TypeScript is that you can write plain JavaScript, and then add the types gradually to make your code more robust. In this tutorial, we are not getting into types for the sake of brevity.

Time to take our plugin for a spin. Launch simple-color-system in Figma and change the source of “Accent UI” from “Blue” to “Green”. See how all the accented elements of our interface update accordingly.

A working plugin! Now we just need to add some design

A working plugin! Now we just need to add some design

Congratulations, in less than a hundred lines of code, spread across two files, we created a fully functioning plugin that makes our flexible color system possible!

Finishing touches

Our plugin works and allows us to repaint our interface at once, without tedious copy-pasting. It’s a big win, but we are designers, after all. We can’t look at the unstyled HTML any longer. Let’s sprinkle our ui.html with some CSS. We will be adding a <style> tag with our CSS rules and edit the markupRow function to add style selectors to our HTML.

Here’s the final ui.html in its entirety:

<!-- ui.html -->
<style>
  html,
  body {
    margin: 0;
    padding: 0;
    font: 11.2px "Helvetica Neue", Arial, sans-serif;
    line-height: 14px;
  }

  #styles-list {
    margin: 0;
    padding: 8px 0;
  }

  .styles-row {
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0 16px;
    list-style: none;
  }

  .assign-operator {
    width: 16px;
    height: 16px;
    color: rgba(0, 0, 0, 0.2);
  }

  .style-source-input {
    /* Reset */
    margin: 0;
    border: none;
    background: none transparent;
    box-shadow: none;
    /* Styles */
    padding: 6px 0 6px 6px;
    border-radius: 1px;
    font: 11.2px "Helvetica Neue", Arial, sans-serif;
    line-height: 14px;
  }

  .style-source-input:hover {
    background-color: white;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
  }

  .style-source-input:focus {
    outline: none;
    background-color: white;
    box-shadow: inset 0 0 0 1px #18a0fb, 0 0 0 1px #18a0fb;
  }

  .flexible-width {
    flex: 1 1 100%;
    min-width: 0;
  }

  .fixed-width {
    flex: 0 0 16px;
  }
</style>

<ul id="styles-list"></ul>

<script>
  function markupRow(receiverSourceData) {
    return `<li class="styles-row">
    <span class="style-receiver flexible-width">${receiverSourceData.receiver.name}</span>
    <span class="assign-operator fixed-width"> = </span>
    <input
      class="style-source-input flexible-width"
      type="text"
      placeholder="None"
      value="${receiverSourceData.source.name}"
      data-receiver-id="${receiverSourceData.receiver.id}"
    >
  </li>`;
  }

  function handleTextFieldChange(event) {
    parent.postMessage(
      {
        pluginMessage: {
          type: "update-source",
          newSourceName: event.target.value,
          receiverId: event.target.dataset.receiverId,
        },
      },
      "*"
    );
  }

  const stylesListElement = document.getElementById("styles-list");
  stylesListElement.addEventListener("change", handleTextFieldChange);

  onmessage = ({ data: { pluginMessage } }) => {
    const messageType = pluginMessage.type;

    if (messageType === "render") {
      const stylesNames = pluginMessage.receiverSourceData;
      const rowHTML = pluginMessage.receiverSourceData.reduce(
        (resultString, pair) => resultString + markupRow(pair),
        ""
      );
      stylesListElement.innerHTML = rowHTML;
    }
  };
</script>

And here’s how our plugin looks like after we styled it!

Now with some style!

Now with some style!

Also, take a look at a final code.ts. We can also add the last bit of code that will reassign paint styles on the plugin launch, so if style descriptions were edited in the Figma interface between the runs of our plugin—the changes will be applied as soon as we run it.

// code.ts

// Helpers

// Look at all styles' descriptions to find a matching source style and return if found
function getSourceForStyle(style, styles) {
  for (const currentStyle of styles) {
    if (style.description === currentStyle.name) return currentStyle;
  }
  return null;
}

// Form pairs
function getReceiverSourcePairs(styles) {
  return styles.map((style) => ({
    receiver: style,
    source: getSourceForStyle(style, styles),
  }));
}

// Main plugin code
figma.showUI(__html__);

const styles = figma.getLocalPaintStyles();
const receiverSourcePairs = getReceiverSourcePairs(styles);

// New! Make sure to update paints  on plugin launch
receiverSourcePairs.forEach((pair) => {
  if (pair.source) {
    pair.receiver.paints = pair.source.paints;
  }
});

const receiverSourceData = receiverSourcePairs.map((pair) => ({
  receiver: {
    name: pair.receiver.name,
    id: pair.receiver.id,
  },
  source: {
    name: pair.source ? pair.source.name : "",
  },
}));

figma.ui.postMessage({
  type: "render",
  receiverSourceData,
});

// Handle message from UI
figma.ui.onmessage = (msg) => {
  if (msg.type == "update-source") {
    const receiverStyle = figma.getStyleById(msg.receiverId) as PaintStyle;
    const sourceStyle = styles.find(
      (style) => style.name === msg.newSourceName
    );
    if (!(receiverStyle && sourceStyle)) return;

    receiverStyle.description = msg.newSourceName;
    receiverStyle.paints = sourceStyle.paints;

    figma.notify(`New source style: ${msg.newSourceName}`);
  }
};

You can also find the final code for the simple-color-system plugin on GitHub.

simple-color-system plugin in action

That concludes our tutorial. Now you can see that adding your little personal features to Figma is no rocket science. All you need is an understanding of the plugin architecture and not being afraid of hacking together some HTML, CSS, and JavaScript.

Our code is far from being production-proof, but it is a good starting point for building features you might need in your designer workflow.


For a designer, writing code is most certainly a step out of a comfort zone—but we believe it is worth taking. Thanks to Figma’s decision to base their product on open web standards, nothing holds us back from experimenting with custom features. We hope could inspire some of the readers to come up with their own plugin ideas and share them with the world, so we can all benefit from each other’s experiments.

The plugin we wrote together is a simplified version of the code we use internally at Evil Martians’ design department. In fact, after using our custom color system for some time, we developed a sister plugin that takes the original idea even further. It allows us to group our linked paint styles into themes—and switch between different themes with a click of the mouse.

This approach already saved our designers hours of work: now we can quickly apply a new coat of paint to complex product mockups, reduce the time between iterations, and ship final results faster.

If you feel like your product can benefit from our design and engineering practices—feel free to drop us a line.

Humans! We come in peace and bring cookies. We also care about your privacy: if you want to know more or withdraw your consent, please see the Privacy Policy.