variables2css – Writing a Figma plugin

blog article variables2css

As developers, we often find ourselves faced with the task of translating design elements like colors, font sizes, and spacing into our code. Alternatively, as designers, we need to summarize and provide this information to the developers. At 9e, this has frequently proven to be quite labor-intensive. Due to this, we sought a more efficient approach for the long term.

The introduction of Figma variables has provided us with precisely this opportunity, as these variables are essentially identical to the ones subsequently created in our CSS file. If we could extract them from Figma, it would significantly reduce our workload. I was enthusiastic about this initial idea, and it's exactly what I wanted to explore further.

Let's Begin: The Figma Plugin Adventure

At the beginning I was not sure what exactly the tool should look like. So first I searched for an existing tool that provides the function of exporting variables for CSS. I found only one plugin that exports variables as JSON only. This gave me my first idea to create a website where users can upload a JSON file and generate variables based on that data.

However, the concept of a website where you upload a JSON file generated by a Figma plugin just to obtain its CSS code seemed like a significant detour to me. Additionally, this approach would create a dependency on that specific plugin, necessitating constant checks for potential changes or lack of support.

So I decided to explore the possibility of creating a Figma plugin. The challenge now was to figure out where to start.

After a quick search, I stumbled upon an introductory tutorial from Figma that provided a solid initial insight into the world of Figma plugins. Figma also offers excellent documentation for its plugin API.

Figma API

After setting up my codebase using the tutorial, I was ready to start. But first, I needed a clear MVP (Minimum Viable Product). The MVP was essential for identifying the functions to prioritize.

For my MVP, users would select their variable collections in Figma, convert these to CSS variables, and then copy the generated code.

This approach helped me estimate the necessary features and understand my requirements from the Figma API. My main goal was to access the collections and individual variables via the API. Thankfully, the API provided several methods to retrieve this information:

Collections & Variables

I am able to obtain an enumeration of all the collections present in the document or

js// getLocalVariableCollections
const localCollections = figma.variables.getLocalVariableCollections();

search specifically for a collection by its ID. However, I only receive the ID from the getLocalVariableCollections() function. Therefore, I required both of these functions.

js// getVariableCollectionById
const collection = figma.variables.getVariableCollectionById('VariableCollectionId');

What you receive from the API in both cases is a Collection-Object, which appears like this.

jsconst variableCollection = {
    id: "VariableCollectionId:1:4",
    defaultModeId: "1:0",
    hiddenFromPublishing: false,
    key: "245b6e0ea91efcd1b77173cd1fbdeb6b6a04936f",
    modes: [
        { name: 'Mode 1', modeId: '1:0' }
    ],
    name: "Primitives",
    remote: false,
    variableIds: [
        /* ... List of variable IDs ... */
    ]
};

This provides information not only about the modes present in the individual collections but also about the variables associated with each collection, as specified in the variableIds.

In the second step, I searched for the variables associated with the respective collection using the variableIds obtained from that collection. For this purpose, the Figma API provides a dedicated function. Additionally, I have included below an example of the object returned by the API.

js//getVariableById
const variable = figma.variables.getVariableById(variableId);
jsconst variable = {
    id: "VariableID:10:337",
    description: /* ... */,
    hiddenFromPublishing: false,
    key: "b638043cd1accac7941f530d286d1c1954b12289",
    name: "color/Gray/750-60%",
    remote: false,
    resolvedType: "COLOR",
    scopes: [
        /* ... List of scopes ... */
    ],
    valuesByMode: {
        "1:0": { r: 0.7450980544090271, g: 0.7450980544090271, b: 0.7450980544090271, a: 0.6000000238418579 }
    },
    variableCollectionId: "VariableCollectionId:1:4"
};

Exactly as I was writing these lines of the blog article, I noticed a minor error in the assignment of variables to their respective modes. The correct approach is to assign the variables based on the ModeId from the collection with the Id found in valuesByMode. However, a different solution had been applied previously. This goes to show that a solid understanding of the API is invaluable.

Once I had established this structure for my plugin, my foundational framework was ready, and I could finally begin.

By the way, with this function, you can also directly retrieve all variables. Personally, I didn't require this functionality, but I included it for the sake of completeness.

js//getLocalVariables
const localVariables = figma.variables.getLocalVariables('STRING'); // filters local variables by the 'STRING' type

My new friend in development

After successfully outputting the variables, my next step was to convert the RGBA data from the object into hex and RGB formats, as well as to specify all pixel (px) values in rems (rem), given its contemporary relevance. Additionally, I aimed to alphabetically sort the output variables. However, for instances where certain variables were very similar and had a numeric value like '0rem–3.125rem', I intended to sort them by size as well, to get something like this:

css:root {
  --spacing-0: 0rem;
  --spacing-2xs: 0.125rem;
  --spacing-xs: 0.3125rem;
  --spacing-s: 0.625rem;
  --spacing-m: 0.9375rem;
  --spacing-l: 1.25rem;
  --spacing-xl: 1.5625rem;
  --spacing-2xl: 1.875rem;
  --spacing-3xl: 2.5rem;
  --spacing-4xl: 3.125rem;
}

Developing this sorting mechanism manually would have been a hefty task for me. While this type of sorting algorithm might be a breeze for some, it posed quite the challenge for me. I'm grateful to ChatGPT for generating the subsequent code, saving me from diving too deep into those complexities.

js// This function sorts an array of objects based on a complex name attribute.
function sortObjectsByAttributeNew(objects) {
  // Sort the objects by splitting their names into parts and comparing them.
  const sortedByName = objects.sort((a, b) => {
    const aParts = a.name.split("/");
    const bParts = b.name.split("/");

    // Get the main names by excluding the last part (value) of the name.
    const aMainName = aParts.slice(0, -1).join("/");
    const bMainName = bParts.slice(0, -1).join("/");

    // Compare the main names, using locale-based comparison.
    if (aMainName !== bMainName) {
      return aMainName.localeCompare(bMainName);
    } else {
      // If main names are the same, compare the last part (value) of the names.
      const aValue = aParts[aParts.length - 1];
      const bValue = bParts[bParts.length - 1];
      const nameComparison = aValue.localeCompare(bValue);
      if (nameComparison !== 0) {
        return nameComparison;
      } else {
        // If last parts of names are also the same, compare values directly.
        return a.value.localeCompare(b.value);
      }
    }
  });

  // Group the sorted objects by their main names.
  const groupedByName = {};
  sortedByName.forEach((obj) => {
    const mainName = obj.name.split("/").slice(0, -1).join("/");
    if (!groupedByName[mainName]) {
      groupedByName[mainName] = [];
    }
    groupedByName[mainName].push(obj);
  });

  // Sort and concatenate the grouped objects, producing the final sorted array.
  const sortedNumberObjects = [];
  for (const mainName in groupedByName) {
    const groupedObjs = groupedByName[mainName];
    const sortedObjs = sortNumberObjectsByValue(groupedObjs);
    sortedNumberObjects.push(...sortedObjs);
  }

  // Return the fully sorted array of objects.
  return sortedNumberObjects;
}

I should mention that this code underwent a complete overhaul during a refactoring process, and this is also where ChatGPT played a role in assisting me.

Creating a Basic UI

After providing the necessary information, I started creating a very simple user interface. Initially, it included only a selection option for the variable collection, a field for displaying the output code, and a button. At the beginning, it is advisable to set a rough height and width for the UI. Additionally, there is an option to enable theme colors (light/dark mode) at this point in the code. Yay! It's these small details that bring me a lot of pleasure.

jsfigma.showUI(__html__, { themeColors: true, width: 400, height: 660 });

If you have theme colors enabled, you can now use the .figma-lightor .figma-dark classes on the <body> element to apply your own theme-specific colors:

css.figma-light body {
  --color: #4f3cec;
  --color-bg: #f1f5fb;
	...
}

.figma-dark body {
	--color: #f1f5fb;
  --color-bg: #4f3cec;
  ...
}

But let's return to the logic. I had to tackle the communication between the UI and the plugin code, comprehending how to transmit information from the UI to the plugin code and vice versa. However, the API also offers very comprehensible functions in this regard:

Sending a message from the UI to the plugin code

html// To send a message from the UI to the plugin code, write the following in your HTML:
<script>
...
parent.postMessage({ pluginMessage: 'anything here' }, '*')
...
</script>
js// To receive the message in the plugin code, write:
figma.ui.onmessage = (message) => {
  console.log("got this from the UI", message)
}

Sending a message from the plugin code to the UI

js// To send a message from the plugin code to the UI, write:
figma.ui.postMessage(42)
html// To receive that message in the UI, write the following in your HTML:
<script>
...
onmessage = (event) => {
  console.log("got this from the plugin code", event.data.pluginMessage)
}
...
</script>

Once I had my plugin code connected to my new UI and vice versa, I made final tweaks to the design and my MVP was ready for release.

First-time Publishing

So now, nothing stood in the way of the release except for finding a good name. Well, after countless seconds of deep contemplation, behold: 'variables2css'! It's as if the name practically assembled itself in a moment of naming genius – describes the function and also has a nice, fancy touch.

I was "happy." I published my first Figma plugin.

Finding Inspiration in Competition

On the same day I noticed that some others had implemented something similar, which discouraged me a bit at first. However, I decided to look at them as potential examples and they actually gave me new motivation to continue improving my plugin and adding new features. So, don't be discouraged if someone else has a similar idea. Instead, take the opportunity to compare, learn from it, and enhance your product based on the new insights you gain.

For example, I found that making the plugin available in development mode allows users to use it without needing an account, but this comes with some limitations. You can only inspect information from the document and cannot interact with it.

js// manifest.json
"capabilities": ["inspect"],
"editorType": ["figma", "dev"],

These two lines of code in the manifest.json elevated my plugin to a new level. Now, you can grant developer access to the file as a designer, and they can utilize the plugin in dev mode to generate the code.

After that, I was even “satisfied” with my plugin, but, as you know, you're never completely content and finished with your own projects. So, more and more features have been added: new color conversions, improved UX, and additional output languages like JavaScript based on Styled Components or Sass. And it still feels like it's not finished; there will be new ideas, and if there aren't any, the code will be refactored, and so on. I'm looking forward to it!

I highly recommend diving into creating your own Figma plugin; it's been a delightful journey for me and continues to be an excellent learning experience.

This blog post was translated with the assistance of DeepL and ChatGPT.