Create your first Figma plugin with Svelte

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
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
Now we have a working base. Let's open it in VSCode:
code .
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
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,
            })
          }
        },
      }
    }
    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"
      },
    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"
    }
    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 })
    This command loads the built template.html.
    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
    And add these imports to the rollup.config.jsfile:
    // 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:
    // 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:
    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:
    <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:
    import App from './Main'
    
    const app = new App({
      target: document.body,
    })
    
    export default app
    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!
    Code in Main.svelte:
    <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.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()
      }
    }
    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
    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
    Let's now add this to your newly created tsconfig.json file:
    {
      "compilerOptions": {
        "baseUrl": ".",
        "target": "esnext",
        "moduleResolution": "node",
        "esModuleInterop": true
      },
      "include": ["./src"]
    }
    Let's now import the rollup TS plugin into rollup.config.js:
    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:
    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:
    # Yarn
    yarn add -D @figma/plugin-typings
    # NPM
    npm install --save-dev @figma/plugin-typings
    Next, add them to the tsconfig.json file:
    {
      "compilerOptions": {
        ...
        "typeRoots": [
          "./node_modules/@types",
          "./node_modules/@figma"
        ]
      }
    }
    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",
    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 πŸ€™

    38

    This website collects cookies to deliver better user experience

    Create your first Figma plugin with Svelte