How to Build Plugins for Figma

If you haven’t heard about Figma, it is a UX/UI design application just like Adobe XD or Sketch. It describes itself as

“The collaborative interface design tool”

I’ve been using it for a while now, mostly for creating web tips like the one below:

How to build query string from key-value pairs

But previously I also used it for its main purpose; Designing user interfaces. Can you guess what takes up the most time when creating a card similar to the one above? — If you guess highlighting the syntax, you were right.

Since I’m using the same colors for the same tokens over and over again, this step is redundant, and not automating it is just wasting your time for no reason.

Fortunately, Figma has plugins that could potentially solve this problem. Unfortunately, I haven’t found one so I had to create my own.


The Concept

First, we need to find out whether it is possible to create such a thing, so I went over to the API documentations on Figma and looked into the TextNode object since I wanted to work with text. It looked like you can color a piece of text using the setRangeFills method, so I came to the following conclusion:

setRangeFills expects a start (inclusive) and end (exclusive) index to know what part of the text should be styled. So first we need to get the selected text, get the tokens from it through regex and find out each token’s start and end position then apply the appropriate style to it.

Note that implementing syntax highlight with regex is not the preferred way. You can’t parse HTML, CSS, or JS with regex because their grammar is much more complex than what regex can handle. In order to cope with the task, you would need a full lexer and parser to identify each token and to make it more robust and act like a real syntax highlighter. However, I wanted to get things done with the least amount of effort for a small set of tokens.


Setting Up The Plugin

In order to start working on plugins, you need to have the desktop application as it can’t be done from the web app. You can download the desktop app here.

Once you have it installed, open it up and go the the “Plugins” option on the left-hand side then click on the plus sign on the right-hand side next to “Development”. It will present you with the following popup:

Figma — new plugin popup

Add a name for your plugin and click on Continue. For the template, we are going to use the default: run once. You can also create an empty project or one with a user interface. It will present you with the following folder structure:

The folder structure

Everything will be done inside the code.ts file. As you can see, Figma uses TypeScript. You can also write your plugin in vanilla JavaScript, in that case, you can skip the following steps and go straight to the next section.

If you don’t have TypeScript installed already, you can install it globally with npm i -g typescript. Using Visual Studio Code, run the “Run Build Task” menu item (ctrlshift + b on Windows) and select tsc: watch — tsconfig.json to compile the project on each save.

If you’re getting the following error even after installing TypeScript globally:

tsc is not a recognized as an internal or external command...

Try to add the following into your system environment variables:

C:\Users\<user_name>\AppData\Roaming\npm

Styling Texts

Initially, code.ts holds a sample plugin. You can delete everything in the file as we are going to build ours from the ground up.

To do some precautions, we will need to check if there is an active selection, and if the selection is a text. We can do this with a simple if statement:

if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'TEXT') {
    // Style text
} else {
    // Show error for user
}
code.ts
Copied to clipboard!

The global figma object is exposed by the API. Once we’re sure we have a selection we can get the whole text by accessing the characters node.

const text = (figma.currentPage.selection[0] as TextNode).characters;
code.ts
Copied to clipboard!

To make sure we don’t get type errors we need to cast the current selection as a TextNode. For styling the text, we can call setRangeFills on it which accepts three params:

Making the whole text white can be done with:

const color = {
    r: 1,
    g: 1,
    b: 1
};

(figma.currentPage.selection[0] as TextNode).setRangeFills(0, text.length, [{ type: 'SOLID',  color }]);
code.ts
Copied to clipboard!

As you can see the value of colors can be between 0 and 1 instead of 0 and 255. To make it easier to work with, let’s set up a function for normalizing values.


Normalizing Colors

To cap values between 0 and 1, all we have to do is divide each value by 255:

const getNormalizedRGB = (r, g, b) => {
    return {
        r: r / 255,
        g: g / 255,
        b: b / 255
    }
};
code.ts
Copied to clipboard!

Using this function, I also set up some predefined colors so they can be easily referenced:

const colours = {
    red: getNormalizedRGB(255, 97, 136),
    green: getNormalizedRGB(169, 220, 118),
    blue: getNormalizedRGB(120, 220, 232),
    yellow: getNormalizedRGB(255, 216, 98),
    purple: getNormalizedRGB(171, 144, 217),
    white: getNormalizedRGB(255, 255, 255),
    base: getNormalizedRGB(131, 144, 147),
    gray: getNormalizedRGB(103, 112, 114)
}
code.ts
Copied to clipboard!

This way, we can simply pass colours.x to the setRangeFills function without having to normalize and write out each color. Still, writing out the whole setRangeFills call can be tedious and we may have to color multiple parts of the text with the same color. To make it a little bit more dynamic, let’s also create a function for the setRangeFills calls.


Making Things Dynamic

Since we may have multiple blocks which require the same colors, we want our function to accept an array of indexes, more specifically a multidimensional array of indexes where each sub-array holds a start and end index like so:

[[0, 10], [23, 42]]

We might also want to pass the color here. This leaves us with the following function:

const applyStyles = (ranges, color) => {
    ranges.forEach(index => {
        (figma.currentPage.selection[0] as TextNode).setRangeFills(index[0], index[1], [{ type: 'SOLID', color }]);
    });
}
code.ts
Copied to clipboard!

We loop through the ranges array and set the start index to index[0] and the end index to index[1]. To try it out, let’s apply a base color to the text with the following function call:

if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'TEXT') {
    const text = (figma.currentPage.selection[0] as TextNode).characters;

    // First apply base colour to whole text, then apply everything else to it
    applyStyles([[0, text.length]], colours.base);
} else {
    // Show error for user
}
code.ts
Copied to clipboard!

We can essentially do the same call for everything else. The only problem which prevents us from doing so is to get the proper index for each block of text. We might want a function that returns a multidimensional array, similar to the one above, that holds each and every start and end index.

Getting text ranges

For getting the indexes, I defined the following function, which takes in a text and a regex pattern which should be run against it:

const getTextRange = (text, regex) => {
    const textRanges = [];

    const results = [...text.matchAll(regex)];

    results.forEach(result => {
        const textIndex = result.index;
        const textLength = result[0].length;

        textRanges.push([
            textIndex,
            textIndex + textLength
        ])
    });

    return textRanges;
}
code.ts
Copied to clipboard!

text.matchAll returns a RegExpStringIterator, which can be transformed into an array using the spread operator. For each result, we get the index of the match — this will be the start index — and the length of the result, which will be the end index.

Adding some regex magic

We’ve left with the regex. This is what will get us the indexes for each text block. To keep everything in one place, I added a regex object at the top of the file which is responsible for holding regex for every possible token and language:

const regexes = {
    html: {
        tag: /<[a-z]+(?=\s)|\/[a-z]+(?=>)/g,
        attributeName: /\s[\w-]+(?=[^<]*>)/g,
        attributeValue: /=[”|"|'][^”|"|']+(?=[”|"|'])/g,
        comments: /<!--([\s\S]*?)-->/g,
    },
    css: {
    
    },
    javascript: {

    }
};
code.ts
Copied to clipboard!

As you can see, most of the regex patterns are ending with a lookahead which helps us to match certain patterns but omit the match in the end result. Unfortunately, lookbehinds are not supported yet so we might have matches where we also select unnecessary parts of the string. For example, the attributeValue will include an equal sign and a quotation mark at the beginning, we don’t want to color them the same way as we do for the attribute value, so we will need to offset the index for some of these patterns.

The way to get around this is to modify the getTextRange function a little bit:

const getTextRange = (text, regex, padding = 1) => {
    const textRanges = [];

    const results = [...text.matchAll(regex)];

    results.forEach(result => {
        const textIndex = result.index;
        const textLength = result[0].length;

        let startIndex = textIndex + padding;

        textRanges.push([
            startIndex < 0 ? 0 : startIndex,
            textIndex + textLength
        ])
    });

    return textRanges;
}
code.ts
Copied to clipboard!

We can introduce a 3rd param called padding. Instead of using textIndex for the start index, we can define a variable that adds padding to textIndex and use that as the start index for the text range. I also added a safety check to avoid going into negative values as we might get errors for that.


Putting Everything Together

Putting everything together, we can apply different styles with the combination of two function calls: applyStyles and getTextRange:

if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'TEXT') {
    const text = (figma.currentPage.selection[0] as TextNode).characters;

    // First apply base colour to whole text, then apply everything else to it
    applyStyles([[0, text.length]], colours.base);

    // Apply HTML styles  
    applyStyles(getTextRange(text, regexes.html.tag), colours.red);
    applyStyles(getTextRange(text, regexes.html.attributeName), colours.blue);
    applyStyles(getTextRange(text, regexes.html.attributeValue, 2), colours.yellow);
    
    // Apply CSS styles
    
    // Apply JS styles
    
} else {
    // Show error for user
}

figma.closePlugin();
code.ts
Copied to clipboard!

We supply getTextRange to the applyStyles function with the text and the required regex. For styling attribute values, we can supply an offset of 2, so =" at the beginning of each attribute won’t be styled with the same color.

Lastly, don’t forget to call figma.closePlugin() as the last thing to terminate the plugin.

To try out the plugin, right-click on your text, select Plugins — Development, and there you will find it. Now the code block will come alive with the click of a button:

the Figma plugin in action
Using the highlighter on HTML markup

Since the writing of this article, a more robust implementation has popped up that uses highlight.js parse logic. You can get the plugin here.

📚 Get access to exclusive content

Want to get access to exclusive content? Support webtips to get access to tips, checklists, cheatsheets, and much more. ☕

Get access Support us
Read more on
🎉 Thank you for subscribing to our newsletter. x