In January of 2020, we launched Ditto's Figma plugin. Since then, it's been used by thousands of people to manage text in their mockups, keep track of edit history and progress, and design with real content!
When we started building Ditto's Figma plugin over a year ago, Figma's plugin development platform was relatively new. We very quickly faced challenges when trying to implement functionality that few other plugins at the time had, like user authentication or frequent calls to a third-party API. Today, the plugin has become a key part of how we create a seamless experience for our users — they can count on Ditto as the central source of copy truth whether they're working out of our web app or their Figma file.
In this post, we'll share our approach to building Ditto's Figma plugin and do a deep dive into how it works under-the-hood.
One of the earliest product decisions we made at Ditto was choosing Figma as our initial integration. As the first realtime collaborative design tool for product teams, Figma made sense as a starting point for achieving our vision at Ditto: to help teams establish a single source of copy truth and make copy a first-class citizen when building product.
In order for Ditto to be a reliable source of truth, we knew that we would need to keep the information in Ditto in sync with what was in Figma. This meant the integration would need to be 2-way: we'd have to be able to send data from Ditto to Figma, and vice versa.
Using Figma's read-only REST API, which had been out for over a year, we could easily fetch information from Figma files. But without write functionality (and no plans from Figma to implement it any time soon), the REST API wasn't going to be enough.
Enter Figma plugins, which had just opened up for beta testing. After some brief exploration, we found that plugins had both read and write access to the Figma file, which meant we would be able to build a plugin to push users' changes in Ditto back to their Figma files. Using Figma's REST API and their plugin API, we'd be able to allow our users to work in web app or the Figma file, without worrying about changes getting out-of-sync or content getting outdated. Within a few days, we got access to the plugin beta, and the rest is history!
We decided to build out an initial version of the plugin using plain javascript. But unlike the simple utility plugins provided as examples by Figma, our plugin needed to support the complex features our existing web app had — things like user authentication, navigation between several pages, regular calls to our Ditto API to read and write data, and dynamic and frequent rendering of large amounts of information in the UI. As a result, our code quickly became unmanageable.
After several conversations and a few painful weeks of developing in plain Javascript, we made the decision to rebuild the entire plugin from the ground up, this time in React. While this set us back several weeks, it was a tradeoff we felt comfortable making to optimize for long-term scalability and faster development down the road. Switching to React allowed us to replace specialized code with reusable components and increase efficiency of UI updates by letting React decide the best way to handle them. It also allowed us to reuse code from our existing web app (which we had built in React) with just a few small tweaks.
Looking back, the time we spent reworking our app has definitely paid off! We've been able to iterate more quickly and maintain a steady stream of improvements for our customers over the past year as a direct result of that decision.
One of the first concepts we had to wrap our minds around when we started building was the Figma plugin execution model. While most aspects of plugin development resemble programming for the web, there is one key difference: plugin code runs in a "sandbox" inside of Figma, rather than the browser itself. As a result, this sandbox thread doesn't have access to the DOM or browser APIs and can't make network requests.
This is where Figma gets creative — we can create an iframe that has access to the browser, which then doubles as the plugin window UI. We can pass messages between our sandbox thread (which has access to the Figma plugin API) and our UI thread (which has access to the browser and all of its APIs). Communication between these two threads is bidirectional, so you can do things like:
We take advantage of both directions of communication frequently in Ditto's plugin. To abstract and simplify this message-passing, we use an event emitter utility created by some fellow plugin developers (thanks Matt and Gleb!). This makes our code cleaner, especially when doing asynchronous messaging via async/await.
Almost every feature in our plugin involves some combination of the following actions:
Each of these can only be done in one of the two threads (sandbox or UI), and we structure our codebase to make that clear. We have three main top-level directories:
We use webpack to bundle everything into a single file at runtime.
Below, we'll explain how each of these three processes works in detail by walking through a feature that's core to Ditto — something we call resyncing.
Whenever a user opens Ditto's Figma plugin, we automatically do a "resync" to fetch the latest changes from the web app and update any outdated text in the Figma file. This involves comparing what's in Ditto to what's in Figma so we can keep the two in sync.
Engineering-wise, resyncing can be broken down into three steps:
Figma's plugin API surfaces the contents of a Figma file via a global object, aptly called figma. The file is represented as a node tree, where each node is a layer that has a type (ex: Page, Frame, Text, etc.) and an ID that's unique within the file.
For Ditto, we mainly care about text nodes and the page and top-level frame that each is on. Starting at the root node, we walk the file tree recursively to find all the nodes of type TEXT, making sure to keep track of the containing PAGE and FRAME nodes.
Once we have the contents of the Figma file, we need to send it via network request to Ditto's resync API endpoint. One problem — you can't actually make network requests from the plugin code! Because of the two-threaded plugin execution model we mentioned earlier, we actually need to pass the data to the UI thread to make the network request.
Using the utility script, we can do that like so:
In the UI thread, we take the message response and send it to Ditto's API's resync endpoint.
It's important to note is that because a Figma plugin runs inside an iframe that has a null origin, it can only make network requests to APIs that allow access to any origin.
The main reason we built a Figma plugin was so users could update their Figma files with the latest Ditto edits. Figma's plugin API has super powerful write functionality — you can programmatically update almost any layer in a Figma file.
For our purposes, we need to update specific text nodes in a Figma file based on the information we receive from the Ditto API.
The Ditto API's resync endpoint returns an object that looks something like this:
Each element of the updates array is an object with the unique ID of the Figma node that needs to get changed and what text it should be changed to.
To update the Figma file, we first send the response to the sandbox thread.
In the sandbox thread, we iterate through the array of updates and use the plugin API to modify the text.
Figma provides two ways to update a text box programmatically: you can set the characters property, or update a specific range of characters. The former sets the styling of all characters to match the first one in the text box, so we do the latter to preserve existing styling as much as possible.
There are a few subtleties here. We first get get character-level diffs between the existing text and the new text using the diffChars method, and then use the Figma plugin API's insertCharacters() and deleteCharacters()functions to update the text node accordingly. We pass 'AFTER' to insertCharacters() so that the style of inserted characters is copied from the following character.
Before modifying text, we have to check whether the user is missing fonts, and if not, load in the necessary fonts. This is a function of Figma being web-based design tool: because font files can be large in number and file size, fonts are loaded at the time of editing.
Once we've finished updating all the nodes, we can send a message back to the UI thread to let any listeners know that we're done (for example, to indicate that resyncing has finished).
With that, we've finished the 3 steps of syncing users' edits in Ditto back to their Figma files! These three processes form the basis of many of the other features of Ditto's plugin, such as the ability to select text in the file and view its Ditto metadata (like status, notes, and tags), batch update text layers, and view and edit all text layers in a selection at once.
Much of what we've built for Ditto's Figma plugin has been uncharted territory, both in terms of copy tooling and Figma plugin development. It's been an exciting journey to grow alongside Figma's own plugin platform and even help shape it as an early plugin in the community.
Ultimately, we believe that design tool integrations are fundamentally redefining how product teams work, and we're so excited that we get to build one that helps make copy part of that conversation! We're incredibly thankful for all our users who have stuck with us and use our plugin every day, and can't wait to show you what else we have in store. :)
If being on the frontlines of design tooling sounds fun to you, learn more about Ditto here — we're hiring!