DEV Community

loading...
Cover image for Create your first Figma plugin with Svelte

Create your first Figma plugin with Svelte

Tom Quinonero
I'm a software engineer working mainly with Design Systems
Originally published at tomquinonero.com ・9 min read

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

While creating Figma Color Manager I needed a building process, SCSS support and reactivity.

I wanted something simple too. So, I decided to go with rollup and svelte.

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

Figma Color Manager on the figma community plugin repository

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:

mkdir <your-plugin-name>
cd <your-plugin-name>
# If using yarn
yarn init
#If using npm
npm init
Enter fullscreen mode Exit fullscreen mode

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

code .
Enter fullscreen mode Exit fullscreen mode

Let's install rollup and some required plugins:

# 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
Enter fullscreen mode Exit fullscreen mode

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:

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,
        })
      }
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Add this to your package.json

"scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public"
  },
Enter fullscreen mode Exit fullscreen mode

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.

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:

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

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:

figma.showUI(__html__, { width: 800, height: 600 })
Enter fullscreen mode Exit fullscreen mode

This command loads the built template.html.

A 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:

yarn add -D svelte svelte-preprocess rollup-plugin-svelte
Enter fullscreen mode Exit fullscreen mode

And add these imports to the rollup.config.jsfile:

// Svelte related
import svelte from 'rollup-plugin-svelte'
import autoPreprocess from 'svelte-preprocess'
Enter fullscreen mode Exit fullscreen mode

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

// 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)
  },
}),
Enter fullscreen mode Exit fullscreen mode

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

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,
        })
      }
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

<input type="text" bind:value>
{value}
Enter fullscreen mode Exit fullscreen mode

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:

import App from './Main'

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

export default app
Enter fullscreen mode Exit fullscreen mode

"Your plugin running in Figma, now using svelte"

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.

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

Code in Main.svelte:

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

<button on:click={handleClick}>Create a Shape</button>
Enter fullscreen mode Exit fullscreen mode

Here we send a createShape message with parent.postMessagewhen the button is clicked.

Code in code.js:

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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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:

# Yarn
yarn add -D node-sass
# NPM
npm install --save-dev node-sass
Enter fullscreen mode Exit fullscreen mode

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:

# Yarn
yarn add -D typescript tslib rollup-plugin-typescript
# NPM
npm install --save-dev typescript tslib rollup-plugin-typescript
Enter fullscreen mode Exit fullscreen mode

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

{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true
  },
  "include": ["./src"]
}
Enter fullscreen mode Exit fullscreen mode

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

import typescript from 'rollup-plugin-typescript'
Enter fullscreen mode Exit fullscreen mode

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:

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,
        })
      }
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

# Yarn
yarn add -D @figma/plugin-typings
# NPM
npm install --save-dev @figma/plugin-typings
Enter fullscreen mode Exit fullscreen mode

Next, add them to the tsconfig.json file:

{
  "compilerOptions": {
    ...
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@figma"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Please refer to the official documentation 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

input: "src/code.ts",
Enter fullscreen mode Exit fullscreen mode

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. Give it a star if you liked it

I'm Tom Quinonero, I write about design systems and CSS,
Follow me on twitter for more tips and resources 🤙

Discussion (0)