Figma Plugin Tutorial (2/6)

How Figma plugins work.

Before we can really go any further we should understand how Figma plugins actually run. Understanding the basic architecture will make the code we are about to write make more sense.

Much of this is summarised from Figma’s own documentation, which is tremendous, so if you prefer you can read that here.

Also feel free to skip to the next part if you don’t care!

⏳ The Sandbox

A plugin runs in a sandbox inside of Figma, isolated from the rest of the application and this has some interesting consequences:

  • Access to the Figma document is exposed through a figma instance in this sandbox. So every time we want to use or manipulate something from the document we call a method or dig into the figma instance. For example, getting the current selection looks something like this:

const selection = figma.currentPage.selection[0]
  • Because the plugin code runs in a sandbox and not in the browser itself (remember even the desktop app is just a browser underneath) we have access to all the latest javascript goodness. It supports ES6 libraries without us having to compile them. No babel 🎉
  • For the same reason, we don’t have access to the browser APIs. This is tricky because we can’t use things like fetch() to get data in the sandbox. The solution to this problem is what defines the architecture of a plugin. Anytime we need access to the browser APIs we have to launch an iframe and send messages to it.
    An iframe is essentially a mini HTML document inside another HTML document but we can think of it like our own little browser window. We can send messages between our sandbox and our iframe to pass data between them.
The original can be found at https://www.figma.com/plugin-docs/how-plugins-run/

The iframe can be visible, for plugins that have a user interface, or invisible. This means you can still have access to the browser APIs even if you don’t want the user to interact with anything.

📄 The Figma Document

The Figma document is represented as a node tree. What does that mean? Well a tree is a type of data structure that looks like this:

Similar to what you find in genealogy with family trees.

There is a root node that contains children nodes. Those children can then in turn have their own children and so on. In Figma, the root node is the document and every layer is a child. That means the above tree would look like this in the layers panel:

Added the “root” group here for demonstration purposes

Notice how things we think of as just being empty containers, like Groups and Frames, are nodes themselves.

🕸 Node Types

Each node in this tree has a type property ie: RECTANGLE, TEXT, GROUP. These types map pretty well to how we think about layers when using Figma. A node’s type dictates what properties it can have. There are properties that are common to all types but there are some properties that are unique to a certain type.

For example: a FRAME type can have a property called .constraints but a node of type RECTANGLE can’t. It’s important to understand this because we need to make our plugins work even if the user selects a node type that we didn’t expect.

We can see a list of all the node types here as well as comprehensive documentation on what properties each type can have.

🕵️‍♀️ Finding the Right Node

Most plugins can be divided into two behaviours:

  • Alters currently selected nodes.
  • Goes through all the nodes and alters ones that meet some criteria.

The first case is fairly straight forward. The figma instance has a currentPage property (each page has it’s own selection) and the currentPage has a selection property ie: figma.currentPage.selection. This returns an array of the currently selected elements.

The second case can be trickier because we need to loop through the document structure to find nodes that match our criteria. Luckily there are some helpful methods for finding nodes:

  • findOne(): Finds the first node that passes the test given as an argument.:
  • findAll(): Finds all the nodes that pass the test given as an argument

// returns the first text element
const node = figma.root.findOne(node => node.type === "TEXT")
// returns an array of text elements
const node = figma.root.findAll(node => node.type === "TEXT")

Note: it’s not always wise to check the entire document as it could be huge.

In both cases we need to make sure we prepare for the possibility that there will be no nodes to work with or more nodes than we anticipated.

📝 Editing a Node

Okay, so now we have the nodes we want to edit, how do we make the changes? Well for the most part it’s pretty simple. We just assign a new value to the property:

const node = figma.root.findOne(node => node.type === "TEXT")
node.textAlignHorizontal = "RIGHT"

Boom, done 🎉.

This works for simple properties but a lot of properties are objects or arrays. For instance the .fills property contains an array of the fills on a particular node. When we change these we need to make a copy of the original array, alter it and then assign it.

🆘 Bad:

const node = figma.root.findOne(node => node.type === "TEXT")
// this assumes we know fills has at least one item
node.fills[0].opacity = 0.5

Good:

const node = figma.root.findOne(node => node.type === "TEXT")
// this assumes we know fills has at least one item
const fills = node.fills
fills.[0].opacity = 0.5
node.fills = fills

There are some interesting technical reasons why it works this way, which you can read about here, but for our purposes we don’t need to know. We only need to remember to always make a copy of the array/object and alter that copy.

Enough waffling, let’s write some plugin code 🎉