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.js
file:
// 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.postMessage
when 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 ๐ค
Top comments (0)