Create your first Figma plugin with Svelte
Tags: figma, svelte, rollup
Published: 2021-07-10 16:35:47


Creating a Figma plugin is easier than you think it is 😀

While creating [Figma Color Manager](https://www.figma.com/community/plugin/971807532885631684/Figma-Color-Manager) I needed a building process, SCSS support and reactivity.

I wanted something simple too. So, I decided to go with [rollup](https://rollupjs.org/guide/en/) and [svelte](https://svelte.dev/).

Svelte is tiny and handle the reactivity at build time instead of at run time like Vue or React does. It offers better performances while keeping similar principles and syntax.

Rollup is a module bundler that is easy to setup. Perfect for our case.

Through this post I'll explain how to setup a similar environment, step by step. Let's go then!

**TLDR: If you don't want to do it yourself, you can just [clone my repository and get started](https://github.com/tomquinonero/figma-plugin-template)**

![Figma Color Manager on the figma community plugin repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ngc5p7ctzakwytfh8t2j.png "Figma Color Manager on the figma community plugin repository (can you find the error?)")

## Prerequisites

You'll need a javascript development environment with `node` and a package manager like `npm` or `yarn`.

It doesn't matter if you use Linux, Mac or WSL on Windows. But you will _not_ be able to test your plugin on the Linux desktop app unfortunately.

If you do not have a JS environment ready type **"**How to install node js on \[Windows/Mac/Linux]**"** on google. and come back when you're all set 😊

You'll need the desktop app to test and debug your plugin.

## Create Rollup Project

This step's goal is to setup the module bundling and have a development server.

Let's create a folder and setup package management:

```shell
mkdir <your-plugin-name>
cd <your-plugin-name>
# If using yarn
yarn init
#If using npm
npm init
```

Now we have a working base. Let's open it in VSCode:

```shell
code .
```

Let's install rollup and some required plugins:

```shell
# yarn
yarn add -D rollup sirv-cli @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-livereload rollup-plugin-terser svelte-preprocess rollup-plugin-postcss rollup-plugin-html-bundle
# NPM
npm install --save-dev rollup sirv-cli @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-livereload rollup-plugin-terser svelte-preprocess rollup-plugin-postcss rollup-plugin-html-bundle
```

Let's configure rollup to build three things:

- The JS code that interact with the Figma API, `code.js`
- The HTML file that displays the UI `template.html`
- The JS code that will be executed on the frontend `main.js`

For this we'll have a special rollup configuration.

First create an `src` folder and add it `code.js`, `template.html` and `main.js`.

Then let's create `rollup.config.js` at your project's root and this should be the content:

```javascript
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import livereload from 'rollup-plugin-livereload'

// Minifier
import { terser } from 'rollup-plugin-terser'

// Post CSS
import postcss from 'rollup-plugin-postcss'

// Inline to single html
import htmlBundle from 'rollup-plugin-html-bundle'

const production = !process.env.ROLLUP_WATCH

export default [
  // MAIN.JS
  // The main JS for the UI, will built and then injected
  // into the template as inline JS for compatibility reasons
  {
    input: 'src/main.js',
    output: {
      format: 'umd',
      name: 'ui',
      file: 'public/bundle.js',
    },
    plugins: [
      // Handle external dependencies and prepare
      // the terrain for svelte later on
      resolve({
        browser: true,
        dedupe: (importee) =>
          importee === 'svelte' || importee.startsWith('svelte/'),
        extensions: ['.svelte', '.mjs', '.js', '.json', '.node', '.ts'],
      }),
      commonjs({ transformMixedEsModules: true }),

      // Post CSS config
      postcss({
        extensions: ['.css'],
      }),

      // This inject the bundled version of main.js
      // into the the template
      htmlBundle({
        template: 'src/template.html',
        target: 'public/index.html',
        inline: true,
      }),

      // If dev mode, serve and livereload
      !production && serve(),
      !production && livereload('public'),

      // If prod mode, we minify
      production && terser(),
    ],
    watch: {
      clearScreen: true,
    },
  },

  // CODE.JS
  // The part that communicate with Figma directly
  // Communicate with main.js via event send/binding
  {
    input: 'src/code.js',
    output: {
      file: 'public/code.js',
      format: 'iife',
      name: 'code',
    },
    plugins: [
      resolve(),
      commonjs({ transformMixedEsModules: true }),
      production && terser(),
    ],
  },
]

function serve() {
  let started = false

  return {
    writeBundle() {
      if (!started) {
        started = true

        require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
          stdio: ['ignore', 'inherit', 'inherit'],
          shell: true,
        })
      }
    },
  }
}
```

After that we need to add NPM scripts and we now have a development environment

Add this to your `package.json`

```json
"scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public"
  },
```

To test the server, you can add a `console.log` to `main.js` and run `yarn dev` or `npm run dev`. It should serve the static built files over [http://localhost:5000](http://localhost:5000/).

This server will not be able to interact with figma but is a good way to work on the UI and svelte components.

## Register as a Figma plugin

To be able to interact with figma we'll register it as a development plugin.

Let's first create a `manifest.json` at the root with the following content:

```json
{
  "name": "<your-plugin-name>",
  "id": "<fill-that-before-publish>",
  "api": "1.0.0",
  "main": "public/code.js",
  "ui": "public/index.html"
}
```

Anywhere on a Figma project, you can `Right click → Plugins → Development → New Plugin...` and the popup will ask you to chose a manifest file.

Select the newly created `manifest.json` and you can now launch your plugin in Figma by doing `Right click → Plugins → Development → <your-plugin-name>` .

It does not does anything yet but Figma acknowledge it and can launch it.

To make the plugin display your UI, add the following to `src/code.js`:

```javascript
figma.showUI(__html__, { width: 800, height: 600 })
```

This command loads the built `template.html`.

![A plugin running in figma](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chd3wwg4b3b9y3atkg4n.png '🎉 Your plugin running in Figma 🎉')

## Adding Svelte

Let's add Svelte! It'll allows our plugin to load reactive components.

For that we need to build `.svelte` files.

Let's first install some needed packages:

```shell
yarn add -D svelte svelte-preprocess rollup-plugin-svelte
```

And add these imports to the `rollup.config.js`file:

```javascript
// Svelte related
import svelte from 'rollup-plugin-svelte'
import autoPreprocess from 'svelte-preprocess'
```

Then look for the `plugin` array around line 32 and paste it that code:

```javascript
// Svelte plugin
svelte({
  // enable run-time checks when not in production
  dev: !production,
  preprocess: autoPreprocess(),
  onwarn: (warning, handler) => {
    const { code, frame } = warning
    if (code === "css-unused-selector" && frame.includes("shape")) return

    handler(warning)
  },
}),
```

Now your `rollup.config.js` should look like this:

```javascript
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import livereload from 'rollup-plugin-livereload'

// Svelte related
import svelte from 'rollup-plugin-svelte'
import autoPreprocess from 'svelte-preprocess'

// Minifier
import { terser } from 'rollup-plugin-terser'

// Post CSS
import postcss from 'rollup-plugin-postcss'

// Inline to single html
import htmlBundle from 'rollup-plugin-html-bundle'

const production = !process.env.ROLLUP_WATCH

export default [
  // MAIN.JS
  // The main JS for the UI, will built and then injected
  // into the template as inline JS for compatibility reasons
  {
    input: 'src/main.js',
    output: {
      format: 'umd',
      name: 'ui',
      file: 'public/bundle.js',
    },
    plugins: [
      // Svelte plugin
      svelte({
        // enable run-time checks when not in production
        dev: !production,
        preprocess: autoPreprocess(),
        onwarn: (warning, handler) => {
          const { code, frame } = warning
          if (code === 'css-unused-selector' && frame.includes('shape')) return

          handler(warning)
        },
      }),

      // Handle external dependencies and prepare
      // the terrain for svelte later on
      resolve({
        browser: true,
        dedupe: (importee) =>
          importee === 'svelte' || importee.startsWith('svelte/'),
        extensions: ['.svelte', '.mjs', '.js', '.json', '.node', '.ts'],
      }),
      commonjs({ transformMixedEsModules: true }),

      // Post CSS config
      postcss({
        extensions: ['.css'],
      }),

      // This inject the bundled version of main.js
      // into the the template
      htmlBundle({
        template: 'src/template.html',
        target: 'public/index.html',
        inline: true,
      }),

      // If dev mode, serve and livereload
      !production && serve(),
      !production && livereload('public'),

      // If prod mode, we minify
      production && terser(),
    ],
    watch: {
      clearScreen: true,
    },
  },

  // CODE.JS
  // The part that communicate with Figma directly
  // Communicate with main.js via event send/binding
  {
    input: 'src/code.js',
    output: {
      file: 'public/code.js',
      format: 'iife',
      name: 'code',
    },
    plugins: [
      resolve(),
      commonjs({ transformMixedEsModules: true }),
      production && terser(),
    ],
  },
]

function serve() {
  let started = false

  return {
    writeBundle() {
      if (!started) {
        started = true

        require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
          stdio: ['ignore', 'inherit', 'inherit'],
          shell: true,
        })
      }
    },
  }
}
```

To test your Svelte app, let's create `src/Main.svelte` and populate it with:

```html
<script>
  let value = "change me!"
</script>

<input type="text" bind:value>
{value}
```

This code will make the content of the input displayed after the field!

To finish load that component as the root component in `main.js`:

```javascript
import App from './Main'

const app = new App({
  target: document.body,
})

export default app
```

!["Your plugin running in Figma, now using svelte"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i0nd464qpnpz5oz9io0l.png "A very simple svelte component running as a figma plugin")

Congratulations! You've created a Figma plugin development environment that'll get you running fast 🤙

**_What?_ 😐
_Are we already done??_ 😒
_How to even interact with Figma????_ 😪
_What about SCSS and Typescript support??_ 😔**

## Going further

### Interacting with Figma

To learn more about how your plugin can interact with Figma, please refer to the [Figma Developers Documentation](https://www.figma.com/plugin-docs/intro/).

To interact with Figma a svelte component sends a message to `code.js` first, `code.js` can then use the Figma API.

As a proof of concept we'll create a blue square in Figma on a button click. Let's check this out!

![A diagram showing how figma plugin separate the code that access the API and the code that displays the UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdrzo05gcb5ethuoymip.png "Separations of concerns in a Figma plugin")

Code in `Main.svelte`:

```html
<script>
  const handleClick = () => {
    parent.postMessage(
      {
        pluginMessage: {
          type: "createShape",
        },
      },
      "*"
    )
  }
</script>

<button on:click={handleClick}>Create a Shape</button>
```

Here we send a `createShape` message with `parent.postMessage`when the button is clicked.

Code in `code.js`:

```javascript
figma.showUI(__html__, { width: 800, height: 600 })

figma.ui.onmessage = (msg) => {
  if (msg.type === 'createShape') {
    // Create a rectangle
    let rectangle = figma.createRectangle()
    // Making it 400x400
    rectangle.resize(400, 400)
    // Making it Blue
    rectangle.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 1 } }]
    // Focus on it
    figma.viewport.scrollAndZoomIntoView([rectangle])
    // Close the plugin
    figma.closePlugin()
  }
}
```

This snippet will create a rectangle in figma when it'll receive the `createShape` event.

You can now interact with Figma!

To know everything you can do, check [How to access the document](https://www.figma.com/plugin-docs/accessing-document/).

### SCSS and styling

In this section we'll take care of the `scss` files support. Also we'll allow to specify a `lang="scss"` in our `style` tags.

Hopefully it's really easy to setup!

Install these dependencies:

```shell
# Yarn
yarn add -D node-sass
# NPM
npm install --save-dev node-sass
```

Annnnd that is pretty much it 🤗

You can now use `<style lang="scss">` in your svelte files and import your global stylesheet with `@import 'main.scss';`.
You could import it in your JS with `import './main.scss';` too.

### TypeScript support

TypeScript will help a lot as you'll have types for the `figma` object. You'll also be able to use TS in your svelte file using `<script lang="ts">`!
_Neat isn't it ?_

Lets add some dependencies:

```shell
# Yarn
yarn add -D typescript tslib rollup-plugin-typescript
# NPM
npm install --save-dev typescript tslib rollup-plugin-typescript
```

Let's now add this to your newly created `tsconfig.json` file:

```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true
  },
  "include": ["./src"]
}
```

Let's now import the rollup TS plugin into `rollup.config.js`:

```javascript
import typescript from 'rollup-plugin-typescript'
```

For the `main.js` compilation we'll add `typescript({ sourceMap: !production }),` after the `commonjs` plugin (around line `56`).
We'll also add this plugin for the `code.js` compilation too (line `97`).

The whole file should now look like that:

```javascript
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import livereload from 'rollup-plugin-livereload'

// Svelte related
import svelte from 'rollup-plugin-svelte'
import autoPreprocess from 'svelte-preprocess'

// Minifier
import { terser } from 'rollup-plugin-terser'

// Post CSS
import postcss from 'rollup-plugin-postcss'

// Inline to single html
import htmlBundle from 'rollup-plugin-html-bundle'

// Typescript
import typescript from 'rollup-plugin-typescript'

const production = !process.env.ROLLUP_WATCH

export default [
  // MAIN.JS
  // The main JS for the UI, will built and then injected
  // into the template as inline JS for compatibility reasons
  {
    input: 'src/main.js',
    output: {
      format: 'umd',
      name: 'ui',
      file: 'public/bundle.js',
    },
    plugins: [
      // Svelte plugin
      svelte({
        // enable run-time checks when not in production
        dev: !production,
        preprocess: autoPreprocess(),
        onwarn: (warning, handler) => {
          const { code, frame } = warning
          if (code === 'css-unused-selector' && frame.includes('shape')) return

          handler(warning)
        },
      }),

      // Handle external dependencies and prepare
      // the terrain for svelte later on
      resolve({
        browser: true,
        dedupe: (importee) =>
          importee === 'svelte' || importee.startsWith('svelte/'),
        extensions: ['.svelte', '.mjs', '.js', '.json', '.node', '.ts'],
      }),
      commonjs({ transformMixedEsModules: true }),

      // Typescript
      typescript({ sourceMap: !production }),

      // Post CSS config
      postcss({
        extensions: ['.css'],
      }),

      // This inject the bundled version of main.js
      // into the the template
      htmlBundle({
        template: 'src/template.html',
        target: 'public/index.html',
        inline: true,
      }),

      // If dev mode, serve and livereload
      !production && serve(),
      !production && livereload('public'),

      // If prod mode, we minify
      production && terser(),
    ],
    watch: {
      clearScreen: true,
    },
  },

  // CODE.JS
  // The part that communicate with Figma directly
  // Communicate with main.js via event send/binding
  {
    input: 'src/code.js',
    output: {
      file: 'public/code.ts',
      format: 'iife',
      name: 'code',
    },
    plugins: [
      typescript(),
      resolve(),
      commonjs({ transformMixedEsModules: true }),
      production && terser(),
    ],
  },
]

function serve() {
  let started = false

  return {
    writeBundle() {
      if (!started) {
        started = true

        require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
          stdio: ['ignore', 'inherit', 'inherit'],
          shell: true,
        })
      }
    },
  }
}
```

We can now use `<script lang="ts">`in our svelte components 🤩🤩🤩

Let's define types for the `figma` object and convert `code.js` to `code.ts`.

Let's install the types:

```shell
# Yarn
yarn add -D @figma/plugin-typings
# NPM
npm install --save-dev @figma/plugin-typings
```

Next, add them to the `tsconfig.json` file:

```json
{
  "compilerOptions": {
    ...
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@figma"
    ]
  }
}
```

Please refer to the [official documentation](https://www.figma.com/plugin-docs/api/typings/) for more details.

We can rename `code.js` into `code.ts`. We'll need to replace the reference to it on `rollup.config.js` line `90`

```jsx
input: "src/code.ts",
```

Restart the server and we now have everything we need!

_Congratulations, you've done your very first figma plugin!_

_Thank you for reading!_

The working code is [available on Github](https://github.com/tomquinonero/figma-plugin-template). Give it a star if you liked it

**_I'm Tom Quinonero, I write about design systems and CSS,_**
**_[Follow me on twitter](https://twitter.com/tomquinonero) for more tips and resources 🤙_**