Tom MacWright

Figma Plugins

At the beginning of 2023, I released a Figma plugin called Placemark, which lets you create vector maps in Figma, the graphic design tool. Since then I’ve been maintaining that plugin for fun, and introduced another one, Placemark Globe.

They’ve been somewhat successful! The Placemark plugin has 11.3k users, according to Figma’s community site, and Placemark Globe has 1.5k users. The feedback loop for plugins is okay - you can see how many likes your plugin has received, and how many “users” there are, but no change-over-time statistics, so I built my own dashboard for the plugins using Observable.

Figma plugin statistics on Observable

But even with that, I don’t really know whether many people are actually using them. Maybe “users” means unique installations - it certainly doesn’t mean active users because the charts I’ve generated go “up only”, and I know that active user numbers are more volatile than that.


I’m pretty fascinated by Figma plugins because of the challenges of sandboxing. Plugins are, by nature, third-party code that has access to a user’s documents, information, and to some extent, their computer. You want a plugin to be powerful, but safe.

Figma has a top-tier engineering team, and they struggled to make this work. They even made a big misstep by first implementing a solution based on Realms that was quickly found to be insecure.

Sandboxing is such an interesting topic because everyone wants it, but there are so few examples of it actually ever working. For example, when I was working on iD, the map editor for OpenStreetMap, we always had the challenge of creating a plugin architecture to rival JOSM, the more established rival which is a Java-based desktop app.

Sandboxing in Observable

Observable is a good example of real-world sandboxing in action. Mike had figured out basically the whole challenge before I got there: the architecture looks like:

  1. User code runs in an iframe with sandbox attributes. The top frame communicates with the iframe through postMessage.
  2. The iframe can communicate back up to the top frame, and user code could maliciously send its own messages (there is no “trusted communication channel”), so any messages from the iframe are limited in their power. Mostly the iframe just tells the top frame the dimensions of its contents.
  3. Each user has a wildcard subdomain that the iframe contents are hosted on, so that cookies can not conflict.

This got us surprisingly far - the Observable sandbox generally just worked, from a security perspective, but it suffered from some other problems. For example, if someone wrote an infinite loop in an Observable Notebook, it would spin the iframe process up to 100% CPU and they’d have to engage a “safe mode” version of the application in order to get rid of the infinite loop. Browser vendors have tinkered with this for a bit now, whether iframes are out of process, or shared with the top page, or maybe all iframes from a certain origin have their own process. It’s a complicated topic.

Sandboxing in Val Town

I wrote a long article about Val Town’s journey to sandboxing recently. In short, the product started off on Node.js’s vm module and wrappers around it, but quickly realized that there was no future to that approach: all of the attempts of sandboxing within a Node.js process were failing.

So, we switched to Deno, the project started by Ryan Dahl, the creator of Node.js. Deno has, from the very beginning, had a permissions model that lets you enable or disable a program’s access to resources. So finally you can do something like installing a module and catching it in the act before it tries to read from your /etc/passwd file, and then actually preventing it from doing so.

Val Town uses node-deno-vm to start up a separate Deno process, which exposes a WebSocket, receives user code, and runs it with Deno’s permissions model. There’s some overhead but it otherwise works really well.

Deno is extremely cool and has worked really well so far.

Sandboxing in Figma

Figma’s eventual solution to the sandboxing problem was to use QuickJS, which is a JavaScript engine that’s small and can be compiled to WASM. In effect, you can run a JavaScript engine in JavaScript with QuickJS.

So basically the plugin architecture runs part of your plugin in an iframe, where you display a UI, and part of it within QuickJS, where that code is able to actually read and write from your Figma document.

Now having written two Figma plugins, I can say that this works. The biggest issues that I’ve run into are debuggability and performance.

Figma crashing

JavaScript’s debugging story is just extremely good. Most of the time when something goes wrong in your code, you get a nice error message that specifies where it happened, and you can use the debugging tools to get more information. I wrote about this back in 2015.

When QuickJS encounters an issue in Figma, you get truly impenetrable errors. I’ve spent a lot of time just guessing what’s going wrong with my plugins, because all I have to go on is something like the stacktrace above.

It’s harder to quantify, but the performance of Figma plugins is also not great. They’ve recently launched dynamic loading, which should improve some performance at the edges, but my plugins still run slower than they feel like they should. Though, of course, I’m making geospatial plugins, and it’s easy to forget that geospatial data has its somewhat unique requirements on systems. Geospatial tools churn through a lot more data than most drawing tools.

Plugin architectures

I should reiterate how amazing it is that Figma has a plugin architecture that allows many people to author plugins. It is extremely rare for this to exist on a website. If I tried to name another website with somewhat freeform community plugins that ran on the frontend, I couldn’t.

I really wish it was easier to build this kind of thing! For all the talk about the web lacking a payments mechanism, we should pay more attention to the fact that the web lacks an extensibility mechanism: we basically have iframes as the only security primitive that enables plugin architectures, and they leave a lot to be desired. WebAssembly is the future (and has been the future for many years), but the story for distribution and debugging of WebAssembly code is still barely written.

Of course, sandboxing in general is a hard issue. There are some success stories - scripting video games in Lua is certainly one of them. The web would be much different and hopefully much better if normal websites were customizable and extensible.

The rest of the plugin authoring experience

Now that I’ve gotten my rant about sandboxing out of the way, what about the rest of the Figma plugin authoring flow?

It’s pretty good! Publishing new versions of Figma plugins is really seamless, just a few clicks, and it’s instantly on the community site. The success is very evident in the Figma Community site that has lots of content. There are lots of other plugins that target mapping, of various levels of quality.

The Placemark Plugin

Note: this post is about a bunch of different topics, as you can probably tell by now, which are tangentially related. I could edit them into something more cohesive, but I am writing for the fun of it and maybe reading this is fun too.

Screenshot of the Figma Plugin page

The Placemark Figma Plugin is by far the more advanced project. It pulls map data straight from Overpass Turbo, which is an API for OpenStreetMap data. This is pretty intentional, relative to the options:

  • Loading maps from a raster tile source would both produce static, raster maps that can’t be further styled, and would also introduce another layer of intellectual property: the styles of a provider like Mapbox or Maptiler are generally copyrighted.
  • Loading tiles from a vector tile source might be a good idea in the long run, but vector tiles are generally optimized for performance and lightness, not completeness. With my plugin currently, you can really get the whole OpenStreetMap data model if you want to, down to adding trees to the map.

So, loading from OpenStreetMap gives the plugin some extra power and simplicity, and it’s pretty clear that the only attribution required is the OpenStreetMap standard attribution, which the plugin adds by default (silence, trolls!)

It’s open source, so you can see how it does all of this. Basically, it implements a tiny static map rendering engine, with label placement, styling, some understanding of OpenStreetMap’s tagging scheme, and more. It uses d3’s implementation of the Mercator map projection, which doesn’t make much of a difference at the local scale.

The plugin has two big weaknesses: water and zoom levels.

Water, as rendered by the plugin

It’s pretty bad at rendering bodies of water. This is a really hard problem in the world of maps: let’s say that you’re rendering a map that shows some coastal town and the ocean is to the right. How do you know that the ocean is to the right? Have you downloaded the entire, enormous, ocean polygon? That’d dramatically slow down your rendering process. There are ways to do this efficiently which are pretty surprising and cool:

The direction the ways are drawn is very important! They must be drawn so that the land is on the left side and water on the right side of the way (when viewing in the direction of the way arrows). If you regard this as tracing around an area of land, e.g., an island, then the coastline way should be running counterclockwise. - from the OpenStreetMap wiki

However, I haven’t found the time to implement coastlines intelligently. It’s open source, so if you feel inspired, I am ready to merge the PR.

The other problem is simply zoom levels: to render a zoomed-out map, I’ll need to craft different Overpass Turbo queries so that the amount of data downloaded is more reasonable, and I haven’t done so yet.

The Placemark Globe Plugin

Screenshot of the Figma Placemark Globe Plugin page

The Placemark Globe plugin is way simpler: it just renders a globe. It doesn’t even render labels yet.

I think that the impetus for creating this was, the usual that “this doesn’t exist yet”, but also that things that are technically simple can still be useful. Someday I’ll find something that’s both simple, useful, and profitable!

This plugin uses d3 more heavily - really, it wouldn’t be possible without all the hard work from d3. The plugin component of it is translating SVG into Figma’s document model and styling system, which is just a bit different than SVG. And making all of that convenient. There are certainly many other ways of generating a vector globe and pulling it into Figma - this is just an easy one.

Because this shows borders, it uses the Visionscarto World Atlas project for data, a modified version of Natural Earth which shows some contentious borders in a more globally-accepted sense. There is a lot written about international borders in cartography that I won’t try to summarize here.

Plugin architecture

I used create-figma-plugin for the Globe plugin initially. I think it has some good patterns. There’s also some great resources for plugin development elsewhere.

Basically, I think I’ve decided that React is overkill for Figma plugins. Nearly any framework is overkill if you just have a few UI elements. Just write some HTML and JavaScript and don’t overthink it. I do wish that there was a standalone CSS framework for these, so that I can get that Figma aesthetic, and dark mode support, right off the bat.

Maintaining these Figma plugins has been pretty fun. It lets me scratch the itch of tinkering with new technology and it helps me keep a little presence in the world of maps, which I still like.