How to quickly develop Sketch, Figma, and Adobe XD design tool plugins
Development Tips

How to quickly develop Sketch, Figma, and Adobe XD design tool plugins

Jakub ZitnyJakub Zitny

A little bit of history: We’ve been building design tool plugins since... well as the first ones on the market most likely. Avocode was actually born from Madebysource, a collective that authored dozens of plugins for designers and developers in the early days of modern web. Long before Avocode we have built the first ever Photoshop plugin CSS Hat. Remember it? (Depends how old are you, haha). Anyway, back in 2014 when React was not a thing, and most of the web designers used Photoshop, we tried to share their pains through photoshopkiller.com. Writing plugins for undocumented formats and tools without plugin APIs was a very different reality than the documented internals of Sketch files that we have nowadays.

Although already in 2015, we have switched focus to Avocode as a standalone application, we still needed to build design tool plugins to help designers sync designs to our platform easily. And as design tools keep changing so our plugins had to keep up.

And today I’d like to describe what we’ve learned so far so if you want to build a design tool plugin yourself you can do it faster.

What you should know before we start:

As a reference, I’ll be talking about our three plugins for Sketch, Adobe XD, and Figma - all of which environments allow similar capabilities. All of these platforms expose JavaScript APIs. Plugin developers usually need access to design data (layers, effects, symbols, and metadata), UI elements and dialogs, and HTTP call possibilities. Packaging and distribution is usually done in a straightforward way within the respective platforms. And that's about it.

Hello design world (from a plugin)

Let's try to look at Sketch, Adobe XD, and Figma ecosystems and build a very simple plugin for all of them. Our plugin will just print out a number of artboards and pages in a design, let's call it My Design Stats.

Before starting, let's make sure we have latest Node.js installed in our system. One more thing, the following commands are written and tested on macOS. They will work very similarly on Linux, and with few simple changes on Windows as well.

Hello from Sketch

A great tool for bootstraping a Sketch plugin (as well as distributing it) is Sketch Plugin Manager, a.k.a skpm. We can use it to generate a directory structure for us, and also copy a bunch of boring files that are necessary for a couple of basic use cases. Let's use a template with preconfigured Prettier.

npm install -g skpm # install skpm

# bootstrap our dir structure, install deps, link plugin to Sketch
skpm create my-design-stats --template=skpm/with-prettier
cd my-design-stats

npm run build # Builds a .sketchplugin package from our code

If you open Sketch.app now, and run Plugins → my-design-stats → my-command, you'll see a simple message on the bottom of your design

Currently, the directory structure that skpm created for us is as follows:

my-design-stats/
    README.md
    my-design-stats.sketchplugin
    package.json
    package-lock.json
    node_modules/
        ... # various dependencies
    src/
        manifest.json
        my-command.js

The interesting part is in the src/ directory, most of our plugin properties are specified in manifest.json — names, entry points, commands, shortcuts, etc. Actions from manifest launch code from my-command.js file (or whatever you specify in the manifest). Right now, we're only using a simple "command" without a UI panel. We click on a menu item, and something happens. For now it's just showing a dummy message, so let's add some content to it.

We'll try to look into the current design and count the number of pages and artboards. We'll be changing the my-command.js for this.

Sketch Javascript API offers a number of imports for various needs. We'll be using dom and ui modules. Sketch design data is internally stored in a tree-like structure called DOM, where you can nest into pages, artboards, and layers, and look at or change stuff. We'll use the ui module for displaying our message. The code looks like this:

const dom = require('sketch/dom')
const ui = require('sketch/ui')

export default function() {
  const doc = dom.getSelectedDocument()

    // NOTE: Count pages right away.
  const pageCount = doc.pages.length

    // NOTE: Add up artboard counts from each page.
  const artboardCount = doc.pages.reduce((count, page) => {
        // NOTE: Internally, artboard is just a top-level layer in each page.
    return count + page.layers.length
  }, 0)

  ui.message(`There are ${pageCount} pages and ${artboardCount} artboards 🎨`)
}

We can run npm run watch in our Terminal, the code we change will automatically rebuild, and latest version will be available in Sketch.

To keep this part simple, let's just change our description, and command name, add an icon and a shortcut for our command. We do all of this in manifest.json.

{
  "description": "Quickly count all pages and artboards in a design",
  "compatibleVersion": 3,
  "bundleVersion": 1,
  "commands": [
    {
      "name": "Count Artboards and Pages",
      "identifier": "my-command-identifier",
      "script": "./my-command.js",
      "shortcut": "cmd shift i"
    }
  ],
  "icon": "./icon.png",
  "menu": {
    "title": "my-design-stats",
    "items": [
      "my-command-identifier"
    ]
  }
}

We'll also need to tell skpm to add the icon to the final plugin package by adding "assets": [ "./icon.png" ] to "skpm" part in package.json. In order to see the changes we did in manifest, we'll need to restart Sketch, or disable and enable our plugin in Plugins → Manage Plugins... menu.

Voila! All code mentioned here is also available in a Github repo.

There are many other things we can specify in the manifest, many modules we can import to our code to get info about our Sketch file, or change it's content. We can look into [Console.app](http://console.app) and filter out Sketch to see logs from our plugin when it's running in Sketch, or use sketch-dev-tools to debug properly. We can even build more complex UIs using web view, React and Typescript.

Did you know a .sketch file is just a ZIP archive with a bunch of JSONs and bitmaps inside? It's actually documented pretty well. But in order to inspect the internals of a Sketch file you don't always need Sketch.app, sketchtool command line tool is here for you 🙂

And one more thing, check out Assistants for forcing designers to be more organized!

Hello from XD

The workflow for XD plugin starts a bit differently. We have to go ahead and sign up to console.adobe.io where we create a project, add XD plugin, and download our folder structure that got generated for us. It looks like this:

9528b83e/ # our plugin ID
    images/
    main.js
    manifest.js

We need to create a develop directory in ~/Library/Application Support/Adobe/Adobe XD and move our downloaded folder (bundle) to it. After that, we refresh plugins in XD plugins page with CMD+SHIFT+R shortcut and we should see our plugin.

It generates a purple rectangle by default, using a scenegraph module. Let's change it to do the same thing as our Sketch plugin. There are no pages in XD, so we'll be counting just artboards.

We'll need to go a bit more low-level than in Sketch. We need to glue a bunch of HTML elements together in order to build our UI. First, we'll just display a dialog with message and a button. This is how main.js will look like.

function countArtboards(selection, root) {
    const dialog = document.createElement("dialog");
    const div = document.createElement("div")
    div.textContent = "Hello XD 🎨"

    const closeButton = document.createElement("button");
    closeButton.textContent = "Close";
    closeButton.addEventListener("click", (ev)=> {
        dialog.close();
    });

    dialog.appendChild(div)
    dialog.appendChild(closeButton)
    document.body.appendChild(dialog).showModal()
}

module.exports = {
    commands: {
        countArtboards,
    }
};

It could be pretty verbose, so the XD Team offers a plugin toolkit library which you can import and use their dialogs. If you're building a bigger app with more complex UI, you can also use React or Vue.

We should also give our plugin a proper name, again in manifest.

{
  "name" : "My Design Stats",
  "host" : {
    "app" : "XD",
    "minVersion" : "21.0"
  },
  "id" : "9528b83e",
  "icons" : [ {
    "path" : "images/icon@1x.png",
    "width" : 24,
    "height" : 24
  }, {
    "path" : "images/icon@2x.png",
    "width" : 48,
    "height" : 48
  } ],
  "uiEntryPoints" : [ {
    "label" : "Count Artboards",
    "type" : "menu",
    "commandId" : "countArtboards"
  } ],
  "version" : "0.0.1"
}

How do we count the artboards now? You might have noticed that XD passes a [selection](https://adobexdplatform.com/plugin-docs/reference/selection.html#selection) and [root](https://adobexdplatform.com/plugin-docs/reference/scenegraph.html#scenegraph) arguments to all exported commands. Both of them contain [scenegraph](https://adobexdplatform.com/plugin-docs/reference/core/scenegraph.html) nodes that we can inspect and change. In this case, we want to look at the root node and count its first-level children — this is the number of all artboards in the document.

div.textContent = `There are ${root.children.length} artboards 🎨`

That's pretty much it for a quick showcase, all bits are in a Github repo. Make sure to package your plugin properly before uploading to Adobe Console. You have to pack it to a ZIP archive, and rename it XDX, otherwise XD won't pick it up as a "plugin bundle". After that, Adobe team will take up to 10 days to approve your thing and list it among other XD plugins 🤷🏻‍♂️

There is a lot more to scenegraph than just counting artboards or layers though — you can do pretty much everything that users can do manually in XD. Besides scenegraph, you can use lower level UXP APIs to communicate with OS, filesystem, or internet; you can use a cloud module to share prototypes and design specs; and you can use Cloud Content APIs to look for your XD files in the cloud.

Hello from Figma

In Figma, you can also scaffold directly from the UI. Open Figma, go to Plugins → Manage Plugins menu and Create new plugin. You choose a template, give it a name, and Figma creates all basic files for you. What's even better, they prefer Typescript right away.

After opening a design, we can see it in Plugins → Development menu. By default it generates 5 rectangles, so let's change it. Before that, make sure to install Typescript, Figma typings, and build the plugin.

npm install --save-dev typescript @figma/plugin-typings
npm run build

Let's change the default behavior to just showing a message like before. The following Figma call is sufficient:

figma.closePlugin('Hello from Figma 🎨')

In order to run this properly, we need to rebuild our code again by running npm run build. After that we can use CMD+OPTION+P shortcut to run last plugin.

In order to add a similar incremental building functionality as we had before in Sketch, we need to add a watch script and install a nodemon library that will make sure to watch for all changes in our code.

npm install --save-dev nodemon

Our package.json now looks like this:

{
  "name": "My-Design-Stats",
  "version": "1.0.0",
  "description": "Your Figma Plugin",
  "main": "code.js",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "watch": "nodemon ./node_modules/.bin/tsc -p tsconfig.json --watch code.ts"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@figma/plugin-typings": "^1.16.1",
    "nodemon": "^2.0.4",
    "typescript": "^4.0.2"
  }
}

Now we can just run npm run watch and our code will automatically recompile after each change.

Figma also has pages like Sketch, and artboards are called frames. We're going to count both. Figma injects a global figma object, where we'll look for a root document node. This node contains tree structure with pages, and their children. Careful here though, pages can contain also layers not connected to frames, so we'll need to filter them out.

const pageCount = figma.root.children.length
const frameCount = figma.root.children.reduce((count: number, page: PageNode) => {
  const frames = page.children.filter((pageChild: SceneNode) => {
    return pageChild.type === 'FRAME'
  })

  return count + frames.length
}, 0)

figma.closePlugin(`There are ${pageCount} pages and ${frameCount} artboards 🎨`)

Pretty straightforward, code is on Github. You can also use React if you need, or call general Figma API from anywhere.

More resources for plugin developers

Here is a quick summary of all the places where you'd want to look if you were to develop an actual plugin for your favorite design tool. Sometimes it could be a bit problematic to find all the right links:

For Sketch

For Adobe XD

For Figma

And if you really need to target Adobe Photoshop or Illustrator (like we do!), look at Common Extensibility Platform which will at some point be replaced by Unified Extensibility Platform that we already mentioned 😉

Hello Avocode

We’ve always been mindful about an inclusive design process — about integrating with major design tools, optimizing for all operating systems, giving back to open-source community, and including all stakeholders in the design process.

Our latest proof of this commitment is a new tool for copywriters and UX writers called Avocode Write. It enables anyone to edit copy in Sketch, XD and Figma designs in the browser. And since we’re talking about plugins, it communicates with our Sketch and Adobe XD plugins to enable designers pull text changes back to the design tool should they need to adjust the text field length or polish the layout. Similar plugin will be available for Figma soon.

So how did we code the new functionality for Avocode Write into our plugins?

Unlike a lot of popular design tool plugins, Avocode wants its own plugins to be very lightweight. We keep most of the heavy-lifting for Avocode app and our Open Design API. So, one of the main things that our current plugins do is communication with Avocode app. And in the case of Avocode Write plugins, this is the functionality that we need:

  • ability to update text layers in a batch
  • support for undo and redo actions (so multiple text layer changes do not add up)
  • sync updated design back to Avocode (to keep the source-of-truth available for the rest of the team)

Let's look at each design tool separately.

Sketch

Communication from Sketch to Avocode works via .avo JSON files with "sync" data. Sketch plugins saves this sync.avo file into a temporary directory, and requests the OS to open the file in Avocode app. The app then handles this "open request" and acts on it.

Communication from Avocode to plugin works by opening a Sketch protocol scheme URL. Avocode app just requests the OS to open the URL and it takes care of the rest.

After Avocode Write plugin gets a signal to update some text layers in the file, it parses the layer and override IDs, and replaces the text and override contents in the DOM-like representation of Sketch design layers. The same one that we used to count pages and artboards before.

We batch all changes into one history item with the following:

const history = doc._object.historyMaker()
history.startCoalescingHistory()
history.registerHistoryMomentTitle('Pull Text Changes from Avocode')

ui.message(`We're pulling text changes from Avocode Write…`)

const expanded = changes.forEach((change) => {
  applyChangeToLayer(change)
})

history.finishCoalescingHistory()

ui.message('Text changes from Avocode Write have been applied')

And we sync updated designs via .avo file to Avocode again. Before proceeding, we actually minify your Sketch file to upload only artboards that have changed from last time. You can version your Sketch files in Avocode, and we automatically detect versions and minimize the changes for you. During our search for diffs in a design, we calculate checksums of parts of the JSONs in Sketch file and figure out which ones were touched. We also traverse all symbols used in changed artboards, and find all artboards that use changed symbols. This way we can always regenerate full Sketch files in our Open Design API. Awesome, right?

Adobe XD

Adobe XD talks to Avocode via internal WebSocket server bidirectionally. XD can send a file to be synced to Avocode, or it can request a set of text changes from Avocode Write. It applies them in a same way as Sketch, but using scenegraph module.

Figma

Figma does not have a support for pulling Avocode Write changes yet, so we'll keep the details for ourselves for now. However, the main Avocode plugin is quite different from our other plugins. It's also the most simple codebase you can imagine. When you sync new frames from Figma to Avocode, we just open a specific "figma-sync" route at app.avocode.com, provide basic data about your needs (like filename, or selected frames), and our Open Design API takes care of everything. It connects to Figma API, extracts everything that we need, and you can open your design in Avocode. We keep our infrastructure secure, so don't worry.

By the way, if you're interested in Open Design API, you can find out more about it here.

Did you like this article? Spread the word!

What is Avocode?

It’s an all-in-one tool for teams that want to code and collaborate on UI design files 2x faster.