I work a lot in Figma, and of­ten times I find my­self with im­ages with sin­gle-col­ored back­grounds. It’s a bit frus­trat­ing be­cause Figma does­n’t have na­tive sup­port for back­ground re­moval [update: it is get­ting one soon!], and the only way to make up for it are plu­g­ins. The few pop­u­lar ones all use some kind of on­line ser­vices, like re­move.bg or the Icons8 Background Remover. They are great plu­g­ins, but they seem to have minds of their own be­cause the re­sults can some­times be abysmal. They also come with lim­its and a price tag, and while I’m not against pay­ing for a ser­vice, I find it a bit ridicu­lous to have to pay for some­thing that should be a ba­sic fea­ture in a de­sign tool. And Photoshop takes too long to open, I’m very lazy.

Another prob­lem is that back­ground re­moval is a bit too smart for its own good. It’s great that it can de­tect the back­ground and re­move it, but some­times it can be a bit too ag­gres­sive and re­move parts of the im­age that I want to keep. I’ve had to man­u­ally fix the im­ages so many times that I’ve lost count. Here’s a demon­stra­tion with two ran­dom pic­tures I found in my Downloads folder:

Example 1 Original
A im­age of the panda from Jujutsu Kaisen - his name is lit­er­ally just Panda
Example 1 Removed
Background re­moved us­ing the Icons8 Background Remover - holy panda what have they done to my boy
Example 1 Original
3D ren­der I made a few years ago while test­ing text in Blender
Example 1 Removed
Background re­moved us­ing the Icons8 Background Remover - it al­most nails it ex­cept for the weird out­lines

What we need is­n’t back­ground re­moval, but sim­ply re­plac­ing or re­mov­ing a color. Sounds easy enough.

But gang, how do we make a Figma plu­gin?

I’ve never made a Figma plu­gin be­fore, but from what I had heard, it was sim­ple JavaScript and some HTML, so this plu­gin should be done in a few hours.

Or so I thought.

It seemed pretty straight­for­ward at first — open the Figma desk­top app and it’ll cre­ate a set of files to get you started. The tem­plate was a plu­gin that opened a win­dow and made a few squares.

Figma plugin file structure
The file struc­ture of a ba­sic Figma plu­gin (source: Figma)

Digging around through the Typescript and HTML files, the plu­gin had a UI (ui.html) that would com­mu­ni­cate with the code.ts file. I could han­dle all the logic in code.ts, and send the out­put to the UI to dis­play. Unfortunately, the Figma plu­gin en­vi­ron­ment is built a bit dif­fer­ently. To quote from their web­site:

For per­for­mance, Figma uses an ex­e­cu­tion model where plu­gin code runs on the main thread in a sand­box. The sand­box is a min­i­mal JavaScript en­vi­ron­ment and does not ex­pose browser APIs. This means that you have all of stan­dard JavaScript ES6+ avail­able, in­clud­ing the stan­dard types, the JSON and Promise APIs, bi­nary types like Uint8Array, etc. But browser APIs like XMLHttpRequest and the DOM are not di­rectly avail­able from the sand­box.

Figma plugin sandbox
The Figma plu­gin sand­box (source: Figma)

The sand­box en­vi­ron­ment had very min­i­mal JavaScript, which meant that I had to do all the heavy lift­ing in the UI. It would­n’t make a sig­nif­i­cant dif­fer­ence in per­for­mance due to the fact that the en­tire plu­gin runs on the Figma client, but it was a bit an­noy­ing to have to write all the logic in the UI. But this was com­plex for one other rea­son — the UI could­n’t com­mu­ni­cate with Figma di­rectly. I had to send mes­sages to the code.ts file, which would then com­mu­ni­cate with Figma.

Now, this was al­ready way more com­pli­cated than I had an­tic­i­pated for some­thing so sim­ple, and TypeScript was­n’t mak­ing it any bet­ter. Using code di­rectly from the ex­am­ples on their web­site gave type er­rors and ESLint warn­ings. At one point, I de­cided to switch to JavaScript but that was even worse be­cause I had to deal with the lack of in­tel­lisense, it was like be­ing blind.

Even worse, my ini­tial im­ple­men­ta­tion of this idea us­ing the can­vas API did­n’t work. My plan was to load the im­age, loop through each pixel, check if the color of the pixel was the tar­get color, and then re­place it with the nec­es­sary color. I still don’t know why it did­n’t work. The code was per­fect in the­ory and the con­sole threw no er­rors. It just did­n’t work. And I re­ally was­n’t feel­ing the vibes there.

With all of that, I gave up. Even ChatGPT gave up.

Round two, fight!

My par­ents did­n’t raise a quit­ter, so I de­cided to give it an­other shot. I started to have a few ideas dur­ing the time I was­n’t work­ing on this. What if I let some­one else do the im­age pro­cess­ing? That way I could fo­cus on com­mu­ni­cat­ing be­tween the UI and Figma. I did­n’t want to use an on­line ser­vice (because then the plu­gin would­n’t work of­fline), but in­stead rely on a tried and tested li­brary.

I found re­place-color which did ex­actly the same. It re­placed a color in an im­age with an­other color. The in­ge­nu­ity of this pack­age, how­ever, was that it sim­ply did not just re­place col­ors. It could also re­place col­ors that were sim­i­lar to the tar­get color, so that the out­put would be much cleaner. This was based on the Delta E al­go­rithm, which cal­cu­lates the dif­fer­ence be­tween two col­ors. The lower the dif­fer­ence, the more sim­i­lar the col­ors are. It was in­sane sci­ence, and I was all for it.

Delta E Perception
0 - 1 Not per­cep­ti­ble by hu­man eyes
1 - 2 Perceptible through close ob­ser­va­tion
2 - 10 Perceptible at a glance
10 - 49 Colors are more sim­i­lar than op­po­site
100 Colors are ex­act op­po­sites

There was one prob­lem, though. The li­brary was built for Node.js, which we knew for sure would­n’t work in the Figma plu­gin en­vi­ron­ment. It turns out re­place-color was built on top of Jimp, a pop­u­lar im­age pro­cess­ing li­brary. The best part — it was writ­ten en­tirely in JavaScript, with zero na­tive de­pen­den­cies, which meant that I could use it in the Figma plu­gin en­vi­ron­ment. This also meant that I had to con­vert re­place-color to work in the browser, as the rest of the JIMP code could han­dle it­self. Looking at the code, it seemed like it was just a few changes to make it work in the browser, with re­plac­ing module.exports with browser-friendly code. It seemed like some­thing I could do man­u­ally, but it turned out quickly that was not the case. The plu­gin seemed to use ad­di­tional de­pen­den­cies, which led to a larger de­pen­dency tree, which I could­n’t man­u­ally con­vert to browser code.

This brought for­ward one of the most fun­da­men­tal prob­lems in web de­vel­op­ment — how do you al­low require in the browser?

Transpiling

Transpiling is the process of con­vert­ing code from one lan­guage to an­other. It’s a com­mon prac­tice in web de­vel­op­ment, where you con­vert TypeScript to JavaScript, or SCSS to CSS. In this case, I wanted to con­vert Node.js code to browser-friendly code. I was­n’t very knowl­edge­able on this front, but a quick Google search told me about Browserify. Browserify is a tool that al­lows you to use require in the browser. It seems to do some more magic than that, but that’s the gist of it.

I made sure to fol­low the in­struc­tions and every­thing, but when I ran the Browserify CLI com­mand, I kept get­ting this an­noy­ing er­ror:

Error: Can't walk dependency graph: Cannot find module [module name]

I would in­stall the mod­ule spec­i­fied in the er­ror, and it would reap­pear with a dif­fer­ent mod­ule’s name. I’ve heard of not be­ing able to walk your dog, but this was my first time of hear­ing about not be­ing able to walk de­pen­dency graphs.

So I de­cided to look for a dif­fer­ent tool. Browserify looked like it had­n’t been up­dated in a while, and so I de­cided to go with one of the best in the busi­ness — Webpack. Although Webpack is of­fi­cially a bundler, it can also do some tran­spiling to bun­dle every­thing up to­gether. They say old is gold, and it could­n’t be truer when it comes to Webpack. While most peo­ple have moved on to newer tools like Vite or Snowpack, Webpack still has a spe­cial place in my heart be­cause of its bulk­i­ness and slow­ness com­pared to light­night-fast Usain Bolt-esque tools like Vite.

// webpack.config.js
const path = require("path");

module.exports = {
entry: "./index.js", // Your package entry point
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
library: "replaceColor", // Global variable for UMD
libraryTarget: "umd", // Universal Module Definition
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // Do not transpile dependencies
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"], // Transpile to browser-compatible JS
},
},
},
],
},
resolve: {
fallback: {
// Add browser-compatible polyfills for Node.js features if required
},
},
};

With that, I ran the Webpack com­mand. To my sur­prise, it worked on the very first try! I now had a 600 KB browser-com­pat­i­ble ver­sion of re­place-color, which I could use in my Figma plu­gin. I was ec­sta­tic, and I could­n’t wait to test it out.

If you ever need a browser (and mini­fied) ver­sion of re­place-color, I gotchu.

GitHub: https://​github.com/​khal­by786/​chro­makey/​blob/​main/​scripts/​re­place-color.js

CDN: https://​cdn.js­de­livr.net/​gh/​khal­by786/​chro­makey/​scripts/​re­place-color.js

Back to Figma

I de­cided to start from scratch. Created a new Figma plu­gin, and started from the ground up. Conveniently enough, I found an of­fi­cial tu­to­r­ial on im­age ma­nip­u­la­tion us­ing Figma plu­g­ins. It was sim­i­lar to what I wanted to im­ple­ment and it sig­nif­i­cantly made things eas­ier. Even then, TypeScript would throw tantrums de­spite me us­ing of­fi­cially doc­u­mented and vet­ted code (I hope no one at Figma is writ­ing tu­to­ri­als off the top of their heads) but I would @ts-expect-error my way through it. To be frank, I’d never heard of @ts-expect-error be­fore, but ESLint sug­gested it over @ts-ignore and I’d give any­thing to get rid of those an­noy­ing squig­gly red lines.

I tried ref­er­enc­ing our new browser-com­pat­i­ble re­place-color in the Figma plu­gin, but it did­n’t work. Script tags could­n’t find the file, and us­ing the the fetch API to load the script and eval­ing it did­n’t work. Disheartened, I de­cided to ask for some help.

Enter Gavin

5 years ago, I was join­ing ran­dom servers for free Nitro give­aways. Now, I was join­ing ran­dom servers for help with my code. I was truly proud of how far I’d come in life. There was a Discord server men­tioned in the docs, and I de­cided to join it. As some­one who has joined way too many servers for help with my code, I tried the trick of search­ing through the mes­sages to see if any­one had the same is­sue. But un­for­tu­nately, no one had the same is­sue, so I had to be the sav­ior for the fu­ture gen­er­a­tions.

Within a few min­utes, some­one called Gavin replied. Gavin was kind enough to in­form that all my scripts had to be in the main UI file for some undis­closed rea­son. He rec­om­mended I use a bundler, but I still had­n’t re­cov­ered from us­ing Webpack, and I cer­tainly was­n’t go­ing to spend more time with it to bun­dle a HTML file of things. Gavin then rec­om­mended I just put all the scripts in­line, which he re­as­sured me was a com­mon prac­tice, and I’ll ad­mit, that had­n’t crossed my mind. It might be a bit messy and dif­fi­cult to work with (but it was just one file), but it cer­tainly would work well.

Discord conversation with Gavin
gavin giv­ing me ther­apy

Final touches

After that, it was just a mat­ter of send­ing Uint8Arrays back and forth, and prop­erly com­mu­ni­cat­ing with the UI. The ac­tual magic took place in the UI, where I would re­cieve the Figma im­age as a Uint8Array, con­vert it to an im­age buffer, then run it through our browser-friendly re­place-color. The out­put would then be sent back to Figma, and voilà!

I was par­tic­u­larly proud of my im­age for­mat de­tec­tion code to set the ap­pro­pri­ate mime type. It was noth­ing too com­plex, but I was just happy I was able to im­ple­ment my comp­sci ma­jor knowl­edge some­where.

// Find the image format by looking at first few bytes
// And then set the appropriate mime type
var imageFormat;
if (
bytes[0] === 137 &&
bytes[1] === 80 &&
bytes[2] === 78 &&
bytes[3] === 71 &&
bytes[4] === 13 &&
bytes[5] === 10 &&
bytes[6] === 26 &&
bytes[7] === 10
) {
imageFormat = "image/png";
} else if (bytes[0] === 255 && bytes[1] === 216) {
imageFormat = "image/jpeg";
} else {
parent.postMessage(
{
pluginMessage: {
type: "error",
message: "Please select a PNG or JPEG file!",
},
},
"*"
);
return;
}

Once I had re­viewed the pub­lish­ing check­list and made fancy ban­ners and demo videos, I was ready to pub­lish!

4 days later

I had ac­ci­den­tally ran Prettier on my HTML file be­fore pub­lish­ing, and that in­cluded the in­line mini­fied script which also got pret­ti­fied and was now 10 times longer. I like to think that that was the rea­son why it took 4 days for the plu­gin to be ap­proved. But it was fi­nally ap­proved, and I was ready to share my rev­o­lu­tion­ary (lmao) plu­gin with the world.

Plugin approval email
duh ofc its gonna work as de­scribed. it bet­ter.

The plu­gin is of­fi­cially called Chroma Key, and it’s avail­able on the Figma Community. It’s free, and it’s open source. I’m not sure if it’s go­ing to be use­ful to any­one, but I’m proud of it. It’s a bit slow, but it works. And that’s all that mat­ters.

Example 1 Removed
Background re­moved us­ing the Icons8 Background Remover
Example 1 Original
Background color re­moved us­ing Chroma Key
Example 1 Removed
Background re­moved us­ing the Icons8 Background Remover
Example 1 Original
Background color re­placed us­ing Chroma Key