For me personally Figma (opens in a new tab) was certainly the tool that changed my webdesign workflow the most, and for the better. Coming from Photoshop it’s the much better tool for the job and it also enabled me way quicker to write plugins for it. If you know HTML, CSS, and JS you can write a Figma plugin! The Figma Developers documentation (opens in a new tab) explains the fundamentals about Figma plugins and also how you can e.g. create one with React. However, in this tutorial you’ll (potentially) step out of your comfort zone and learn how to use Svelte (opens in a new tab), TypeScript (opens in a new tab), and Rollup (opens in a new tab) to write a plugin for Figma.
The popularity of Svelte is rising, more and more people enjoy writing smaller to bigger apps with it. So it’s certainly good to have it in your toolset and to have some familiarity with it. Coming from React I most enjoyed the “simplicity” to some aspects of writing code in Svelte, as where React felt really verbose and clunky in comparison. Another thing you’ll learn to appreciate is the output and speed of Svelte apps. Lastly, why should you care about Figma? Well, it’s a super popular tool amongst individuals and larger teams – and super extensible with plugins.
Here’s what you’ll build. You can pull a random image from Unsplash (opens in a new tab) in the given size:
The finished project is available on GitHub at figma-plugin-svelte-example (opens in a new tab). You can also use this as a template for your next project.
If you want to follow this tutorial step-by-step you’ll need to have a Figma Account (Sign Up for an account (opens in a new tab)) and the desktop app installed (download (opens in a new tab)).
Here’s what you’ll use and build explained on a higher level as a short summary:
- Inside Figma initialize a new plugin to create the necessary files and learn how to run a local plugin
- Add dependencies like Svelte, Rollup, TypeScript, and Figma Plugin DS Svelte (opens in a new tab) to build the app with hot-reloading support and modern output
- Use the Figma Plugin DS Svelte UI kit to build out the UI
- Leverage the browser
fetch
API to pull a random image from Unsplash and place it onto the current Figma frame using Figma’s plugin APIs
Open the Figma desktop app and if you haven’t already log into your account. Create a new design file in your drafts as you’ll use this as a testground for the plugin. Once you have the design file open, go to Menu => Plugins => Development => New Plugin….
First, it’ll ask for a name (choose the name “SvelteTutorial”) and whether you want to create a “Figma design + Figjam” or “Figma design” plugin. Choose “Figma Design”. In the next step, choose the “Empty” template. The “Save As” dialogue asks you for a directory where you want to create the project. Figma will create a folder with the given name inside your provided folder. Once that’s done you should see a code.js
and manifest.json
file inside the newly created folder. Delete the code.js
file, it’ll be autogenerated later.
The manifest.json
will contain something like this:
{ "name": "SvelteTutorial", "id": "1234567890123456789", "api": "1.0.0", "main": "code.js", "editorType": ["figma"]}
You can learn more about the manifest.json
file in the Figma Plugin Manifest documentation (opens in a new tab) but in a nutshell: This is the file with the plugin’s unique ID (assigned during the New Plugin… flow) and settings regarding the plugin. main
points to the JavaScript code of our plugin. ui
specifies the HTML file used in the iframe modal.
Add a new key called ui
to manifest.json
and change the location of main
:
{ "name": "SvelteTutorial", "id": "1234567890123456789", "api": "1.0.0", "main": "dist/code.js", "ui": "dist/ui.html", "editorType": ["figma"]}
Since you’ll be using TypeScript to write the source files and Rollup to compile the files into a dist
folder, the manifest.json
now points to the (soon to be) compiled files.
Initialize a new package.json
with npm init -y
and add the following dependencies (use the “Copy” button for easier installation):
npm install @figma/plugin-typings @rollup/plugin-commonjs @rollup/plugin-html @rollup/plugin-node-resolve @rollup/plugin-typescript cssnano figma-plugin-ds-svelte postcss rollup rollup-plugin-postcss rollup-plugin-svelte rollup-plugin-svg rollup-plugin-terser svelte tslib typescript
Also, create a .gitignore
will the following contents:
node_modulesdist.cache
Since you’ll use TypeScript a tsconfig.json
for the compiler is necessary. You can use this configuration:
{ "compilerOptions": { "target": "es6", "esModuleInterop": true, "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "importsNotUsedAsValues": "error", "skipLibCheck": true, "typeRoots": ["./node_modules/@types", "./node_modules/@figma"] }, "include": ["./src/**/*"]}
Lastly, add scripts
to your package.json
to compile files on the fly in development mode and for production when you want to publish:
{ "scripts": { "build": "rollup -c", "dev": "rollup -c -w", "clean": "rm -rf dist", "typecheck": "tsc --noEmit" }}
In these next steps you’ll create the source files that will be compiled by Rollup and the configuration for Rollup itself.
Start by creating a directory called src
and create three files in this directory: code.ts
, main.ts
, and App.svelte
.
The code.ts
will be responsible for creating the iframe and listening for messages. For now it only needs to contain this:
figma.showUI(__html__, { width: 320, height: 220 })
From Figma’s API documentation (opens in a new tab) about showUI
:
Enables you to render UI to interact with the user, or simply to access browser APIs. This function creates a modal dialog with an iframe containing the HTML markup in the html argument.
The __html__
is a global variable that is getting defined through the ui
key in manifest.json
.
In order to load App.svelte
the main.ts
file sets up the app onto the document.body
:
import "svelte"import App from "./App.svelte"const app = new App({ target: document.body,})export default app
And now you can write Svelte code! Start with – of course 😉 – a “Hello World”:
<div>Hello World</div>
The last missing piece to this is rollup.config.js
. It’ll define how these three source files are bundled into something that Figma can work with:
import resolve from "@rollup/plugin-node-resolve"import commonjs from "@rollup/plugin-commonjs"import typescript from "@rollup/plugin-typescript"import html from "@rollup/plugin-html"import svelte from "rollup-plugin-svelte"import { terser } from "rollup-plugin-terser"import svg from "rollup-plugin-svg"import postcss from "rollup-plugin-postcss"import cssnano from "cssnano"const production = !process.env.ROLLUP_WATCH/** * @type {import('rollup').RollupOptions} */const mainConfig = { input: `src/main.ts`, output: { format: `iife`, name: `ui`, file: `dist/bundle.js`, }, plugins: [ typescript(), svelte({ compilerOptions: { dev: !production, }, }), resolve({ browser: true, dedupe: (importee) => importee === `svelte` || importee.startsWith(`svelte/`), }), commonjs(), svg(), postcss({ extensions: [`.css`], plugins: [cssnano()], }), html({ fileName: `ui.html`, template({ bundle }) { return `<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <title>Your Name</title> </head> <body> <script>${bundle[`bundle.js`].code}</script> </body></html> ` }, }), production && terser(), ], watch: { clearScreen: false, },}/** * @type {import('rollup').RollupOptions} */const codeConfig = { input: `src/code.ts`, output: { file: `dist/code.js`, format: `cjs`, name: `code`, }, plugins: [ typescript(), commonjs(), resolve({ browser: true, }), production && terser(), ],}const config = [mainConfig, codeConfig]export default config
I don’t want to focus too much on Rollup in this guide as it’s just a build tool. If you have specific questions about this setup, feel free to ask me on Bluesky (opens in a new tab).
Now it’s time you try out what you have so far in Figma. In your terminal, run:
npm run dev
You should see some output in the terminal that signals that Rollup successfully compiled the files you created:
$ rollup -c -wrollup v2.67.3bundles src/main.ts → dist/bundle.js...created dist/bundle.js in 573msbundles src/code.ts → dist/code.js...created dist/code.js in 357ms
The manifest.json
now points to these compiled files. Go back to Figma and open the draft design file that you created earlier. You now can run your plugin by doing a Right click => Plugins => Development => SvelteTutorial. You should a new window pop-up with the name of your plugin and a “Hello World” as text.
Congrats, you successfully set up a boilerplate for creating Figma plugins with Svelte! You now know how to initialize a new plugin from Figma and how to configure the manifest.json
file. After that you’ve installed Rollup to compile Svelte and TypeScript files.
Now that you have a barebones project ready, it’s time for you to create the UI and add functionality to it. As a reminder: The goal of the plugin (and thus the UI) is to fetch a random image from Unsplash in a specific size (width & height) and place it onto the current Figma canvas. For this you’ll learn how to use Figma Plugin DS Svelte (opens in a new tab) as a UI kit, how to fetch images, and how to use Figma’s API to place images.
The Figma Plugin DS Svelte will serve you in two ways:
- It has an extensive list (opens in a new tab) of CSS variables and utility classes (similar to Tailwind CSS) to help build UIs with your own set of components
- It features a bunch of components like Buttons, Menus, Switches, etc. to build UIs quicker
In order to use Figma Plugin DS Svelte you’ll need to add its GlobalCSS
import to your App.svelte
to have the CSS styles available.
<script lang="ts"> import { GlobalCSS } from "figma-plugin-ds-svelte"</script>
Then you have the utility classes (similar to Tailwind CSS) and CSS variables available. Create a wrapper component for the contents that will follow:
<script lang="ts"> import { GlobalCSS } from "figma-plugin-ds-svelte"</script><div class="p-xsmall wrapper flex column justify-content-between"> <div> Contents </div> <div class="mt-small footer pt-xxsmall"> Footer </div></div><style> .wrapper { text-align: center; height: 100%; } .footer { border-top: 1px solid var(--silver); }</style>
This is the syntax you should be familiar with when using Svelte. When you run the plugin inside Figma again, you should see “Contents” and “Footer” aligned and formatted in the pop-up box.
In the rest of this tutorial the <style>
block inside App.svelte
won’t change anymore. For the sake of keeping code snippets more concise the <style>
block will be omitted from any intermediate steps. The end result will contain everything again.
Time to add some inputs. For this, add the Type
, Label
, and Button
component to your App.svelte
:
<script lang="ts"> import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte"</script><div class="p-xsmall wrapper flex column justify-content-between"> <div> <Type> Get a Random Image from <a href="https://unsplash.com/">Unsplash</a> </Type> <div class="flex mt-xsmall"> <div class="flex ml-xxsmall mr-xxsmall"> <Label>Width:</Label> Input </div> <div class="flex ml-xxsmall mr-xxsmall"> <Label>Height:</Label> Input </div> </div> <div class="mt-xxsmall flex justify-content-center"> <Button>Add Image</Button> </div> </div> <div class="mt-small footer pt-xxsmall"> <Type> Read the tutorial <a href="https://www.lekoarts.de">on lekoarts.de</a> </Type> </div></div>
The UI is nearly done! When you run the plugin again inside Figma you’ll see the two inputs for width and height horizontally aligned with the blue “Add Image” button below them. Now it’s time to add the proper Input
to your UI.
At the time of writing this guide the Input
component (opens in a new tab) from figma-plugin-ds-svelte
didn’t offer the possibility to define the type
on the <input />
(it defaults to type="input"
). In order to be able to built-in validations for numbers the type
has to be number
and thus you need to “fork” the component. Or in other words: Create your own Input
component by copying the contents and adapting it to use type="number"
.
Create a new file called Input.svelte
inside src
:
<script lang="ts"> export let id = null export let value = null export let name = null export let placeholder = "Placeholder"</script><input type="number" {name} required min="0" {id} bind:value {placeholder} /><style> input { font-size: var(--font-size-xsmall); font-weight: var(--font-weight-normal); letter-spacing: var(--font-letter-spacing-neg-xsmall); line-height: var(--line-height); position: relative; display: flex; overflow: visible; align-items: center; width: 100%; height: 30px; margin: 1px 0 1px 0; padding: var(--size-xxsmall) var(--size-xxxsmall); color: var(--black8); border: 1px solid transparent; border-radius: var(--border-radius-small); outline: none; background-color: var(--white); } input:hover, input:placeholder-shown:hover { color: var(--black8); border: 1px solid var(--black1); background-image: none; } input::selection { color: var(--black); background-color: var(--blue3); } input::placeholder { color: var(--black3); border: 1px solid transparent; } input:placeholder-shown { color: var(--black8); border: 1px solid var(--black1); background-image: none; } input:focus:placeholder-shown { border: 1px solid var(--blue); outline: 1px solid var(--blue); outline-offset: -2px; } input:active, input:focus { color: var(--black); border: 1px solid var(--blue); outline: 1px solid var(--blue); outline-offset: -2px; }</style>
It’s not exactly the same as the <Input />
component from figma-plugin-ds-svelte
as I removed some CSS styles that are not needed for this example. The CSS uses the custom properties defined globally.
Now it’s time to use the <Input />
component in your App.svelte
:
<script lang="ts"> import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte" import Input from "./Input.svelte" let disabled = true let width = 800 let height = 800 $: disabled = !(!!width && !!height)</script><div class="p-xsmall wrapper flex column justify-content-between"> <div> <Type> Get a Random Image from <a href="https://unsplash.com/">Unsplash</a> </Type> <div class="flex mt-xsmall"> <div class="flex ml-xxsmall mr-xxsmall"> <Label>Width:</Label> <Input placeholder="800" bind:value={width} /> </div> <div class="flex ml-xxsmall mr-xxsmall"> <Label>Height:</Label> <Input placeholder="800" bind:value={height} /> </div> </div> <div class="mt-xxsmall flex justify-content-center"> <Button {disabled}>Add Image</Button> </div> </div> <div class="mt-small footer pt-xxsmall"> <Type> Read the tutorial <a href="https://www.lekoarts.de">on lekoarts.de</a> </Type> </div></div>
You’re binding the value that is coming from the inputs to variables called width
and height
. They are necessary when adding an image and thus the “Add Image” button is disabled if one of them or both are not defined. You can try this yourself by running the plugin. You should see that the “Add Image” button is faded out and only when you add both values it gets solid.
The disabled
property to control this behavior is a reactive declaration (opens in a new tab). It tells Svelte to re-run this code whenever one of these values changes (so it coerces the boolean when width
and height
changes again and again).
You can use multiple ways of styling and CSS with Svelte, in this example you’ve used a combination of global CSS (provided by a third-party package) and defining your own <style>
blocks. You’ve also created your own <Input />
component to correctly handle number inputs. Lastly, using the reactive declaration syntax the “Add Image” button gets disabled if not every necessary input is provided.
Now that the UI is in place, the last task is to wire up the business logic to add an image onto the canvas. For this you’ll use the Figma plugin API figma.ui.onmessage
to listen for calls in the UI. The Svelte UI will call this handler after fetching data from Unsplash’s API. There are some interesting quirks when creating an image in Figma, you’ll learn those when creating the createImage
function.
To get started, create an async function called getImage
and define a API_URL
constant inside your App.svelte
:
<script lang="ts"> import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte" import Input from "./Input.svelte" const API_URL = "https://source.unsplash.com/random/" let disabled = true let width = 800 let height = 800 $: disabled = !(!!width && !!height) async function getImage() {}</script>
Here’s what the function will do:
- Validate that the necessary input (
width
andheight
) is valid - Call the
/random
API endpoint from Unsplash to get redirected to a random image - Fetch the image that was chosen by the
/random
endpoint - Return the image as
Uint8Array
since thecreateImage
plugin API (opens in a new tab) requires this as input
The complete function should look like this:
async function getImage() { if (Math.sign(width) === -1 || Math.sign(height) === -1) { throw new Error("Enter positive values for height/width") } // It redirects to the URL of the actual image const initialResponse = await self.fetch(`${API_URL}${width}x${height}`) if (initialResponse.ok) { // Fetch the actual image const res = await self.fetch(initialResponse.url) const buffer = await res.arrayBuffer() return new Uint8Array(buffer) } else { throw new Error(initialResponse.statusText) }}
You can now successfully fetch a random image from Unsplash 🎉 As a next step you have to add an addImage
function that is called when you press the “Add Image” button in the UI.
Fetching data from remote APIs should always be handled gracefully since a lot of things can go wrong. User input might be wrong, your user is offline, or the remote API itself has problems. To not let the user waiting without any information it’s good UX to add loading and error states to your UI.
Add new variables called error
, loading
, and imageBytes
to App.svelte
. Also add an async function called addImage
:
<script lang="ts"> import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte" import Input from "./Input.svelte" const API_URL = "https://source.unsplash.com/random/" let disabled = true let error = "" let loading = false let imageBytes = null let width = 800 let height = 800 $: disabled = !(!!width && !!height) async function getImage() {/* Function body */} async function addImage() {}</script>
The addImage
function will do the following:
- Reset the
error
state to an empty array (so that subsequent errors are not merged when you press the “Add Image” multiple times) - Set the
loading
state totrue
before doing any actual work - In a
try/catch
block (to gracefully handle any errors) use thegetImage()
function to set theimageBytes
variable - If an error occurs, the
error
is populated with an error message - Send the
imageBytes
and other metadata to the UI’siframe
window viapostMessage
(opens in a new tab)
The last item in this list is probably the most important piece of this function. The postMessage
function is the interface to communicate any arbitrary information from your UI to Figma’s onmessage
API.
The first argument of postMessage
can be almost any data type or plain object, as long as it’s a serializable object (more fundamentals available in Creating User Interfaces (opens in a new tab)). I’ve chosen to model the message that is sent as a “Redux Action”-like format of an object with a type
and payload
. This way you can use multiple different postMessage
in the future if you have a need for that.
Here’s the code for the addImage
function:
async function addImage() { error = "" loading = true try { const result = await getImage() loading = false imageBytes = result } catch (errorMsg) { loading = false error = errorMsg return } parent.postMessage( { pluginMessage: { type: "ADD_IMAGE", payload: { imageBytes, width, height, }, }, }, "*" )}
Hang tight, only one more things is necessary to finish up the Svelte functionality. Call the addImage
function when clicking the “Add Image” button and conditionally show loading or error states.
You can do so by adding an on:click
handler to the button:
<Button {disabled} on:click={addImage}>Add Image</Button>
Below the button you can show the loading and error states:
<div class="mt-xxsmall flex justify-content-center"> <Button {disabled} on:click={addImage}>Add Image</Button></div>{#if loading} <div class="mt-xxsmall"><Type weight="medium">Loading image...</Type></div>{/if}{#if error} <div class="mt-xxsmall"><Type color="red" weight="medium">{error}</Type></div>{/if}
Your App.svelte
is now complete and should look like this:
<script lang="ts"> import { GlobalCSS, Type, Label, Button } from "figma-plugin-ds-svelte" import Input from "./Input.svelte" const API_URL = "https://source.unsplash.com/random/" let disabled = true let error = "" let loading = false let imageBytes = null let width = 800 let height = 800 $: disabled = !(!!width && !!height) async function getImage() { if (Math.sign(width) === -1 || Math.sign(height) === -1) { throw new Error("Enter positive values for height/width") } // It redirects to the URL of the actual image const initialResponse = await self.fetch(`${API_URL}${width}x${height}`) if (initialResponse.ok) { // Fetch the actual image const res = await self.fetch(initialResponse.url) const buffer = await res.arrayBuffer() return new Uint8Array(buffer) } else { throw new Error(initialResponse.statusText) } } async function addImage() { error = "" loading = true try { const result = await getImage() loading = false imageBytes = result } catch (errorMsg) { loading = false error = errorMsg return } parent.postMessage( { pluginMessage: { type: "ADD_IMAGE", payload: { imageBytes, width, height, }, }, }, "*" ) }</script><div class="p-xsmall wrapper flex column justify-content-between"> <div> <Type> Get a Random Image from <a href="https://unsplash.com/">Unsplash</a> </Type> <div class="flex mt-xsmall"> <div class="flex ml-xxsmall mr-xxsmall"> <Label>Width:</Label> <Input placeholder="800" bind:value={width} /> </div> <div class="flex ml-xxsmall mr-xxsmall"> <Label>Height:</Label> <Input placeholder="800" bind:value={height} /> </div> </div> <div class="mt-xxsmall flex justify-content-center"> <Button {disabled} on:click={addImage}>Add Image</Button> </div> {#if loading} <div class="mt-xxsmall"><Type weight="medium">Loading image...</Type></div> {/if} {#if error} <div class="mt-xxsmall"><Type color="red" weight="medium">{error}</Type></div> {/if} </div> <div class="mt-small footer pt-xxsmall"> <Type> Read the tutorial <a href="https://www.lekoarts.de">on lekoarts.de</a> </Type> </div></div><style> .wrapper { text-align: center; height: 100%; } .footer { border-top: 1px solid var(--silver); }</style>
You successfully finished the Svelte portion of this tutorial! Everything is now wired up to the Figma plugin API and in the last part you can place images onto the canvas.
You’ve learned how to use Unsplash’s /random
API to fetch an image and returning it in a format that Figma understands. In the next step you added loading and error logic to gracefully handle those API calls. Lastly, you’ve used Figma’s postMessage
handler to communicate the image information to Figma.
You can now try out what you have so far in Figma. If you enter valid entries for width
and height
, you should get a loading state. If you e.g. enter -1
for width
you should see an error message.
Remember the code.ts
file you created at the beginning? It’s now time to come back to this file. With the onmessage
handler you’ll use the message that the UI sends via the postMessage
function to place images onto the canvas. For this you learn how to create a reusable createImage
helper function.
But let’s first verify, that the postMessage
works successfully. In your code.ts
file, add a handler for onmessage
and log out the arguments:
figma.showUI(__html__, { width: 320, height: 220 })figma.ui.onmessage = (msg) => { console.log(msg) figma.closePlugin()}
The figma.closePlugin()
function (as the name suggests) closes the plugin once this handler is called. You can comment this out for now for debugging purposes if you want.
Go to Figma and open the DevTools via Menu => Plugins => Development => Open console. Open your plugin and press the “Add Image” button. You should see something like this in the console now:
{ type: 'ADD_IMAGE', payload: { height: 800, width: 800, imageBytes: [/* Array of bytes */] }}
If this didn’t work, restart rollup with npm run dev
, close the plugin in Figma, and try again.
Instead of logging out the response, handle the ADD_IMAGE
type:
interface IPayload { imageBytes: Uint8Array width?: number height?: number}interface IMessage { type: "ADD_IMAGE" payload: IPayload}figma.ui.onmessage = (msg: IMessage) => { const { type, payload } = msg if (type === `ADD_IMAGE`) { if (payload.imageBytes) { const { imageBytes, width, height } = payload // createImage({ imageBytes, width, height }) } } figma.closePlugin()}
Your last task is to author the createImage
function. Here’s what it’ll do:
- Call Figma’s
createImage
API (opens in a new tab) to create anImage
object. This quote from the API docs is quite important:Note that Image objects are not nodes. They are handles to images stored by Figma. Frame backgrounds, or fills of shapes (e.g. a rectangle) may contain images.
- Create a
fill
object with the result ofcreateImage
.fill
is used since images in Figma are not layers but fill types for a shape - Create a rectangle with the size of given
width
andheight
input. Mark the position as the center of the canvas - Apply the previously created
fill
type to the rectangle - Append the rectangle to the canvas and zoom + focus on it
The complete code.ts
should look like this:
figma.showUI(__html__, { width: 320, height: 220 })interface IPayload { imageBytes: Uint8Array width?: number height?: number}interface IMessage { type: "ADD_IMAGE" payload: IPayload}function createImage({ imageBytes, width = 800, height = 800,}: IPayload): void { const image = figma.createImage(imageBytes) const imageHash = image.hash // An image in Figma is not a layer, but a fill type for a shape const fill: ImagePaint = { type: "IMAGE", imageHash, scaleMode: "FILL", } const rect = figma.createRectangle() rect.resize(width, height) rect.x = figma.viewport.center.x - Math.round(width / 2) rect.y = figma.viewport.center.y - Math.round(height / 2) rect.name = `Random Unsplash Image` rect.fills = [fill] figma.currentPage.appendChild(rect) figma.currentPage.selection = [rect] figma.viewport.scrollAndZoomIntoView([rect])}figma.ui.onmessage = (msg: IMessage) => { const { type, payload } = msg if (type === `ADD_IMAGE`) { if (payload.imageBytes) { const { imageBytes, width, height } = payload createImage({ imageBytes, width, height }) } } figma.closePlugin()}
Try it out! You should get an image onto your canvas now when pressing the “Add Image” button 🎉
You’ve finished this tutorial, congrats! Using the createImage
API from Figma you created an Image
object. Then you’ve applied this as a fill to a shape (since images in Figma are shapes with a image fill type) and then wired it all up with the UI.
This section is totally optional, you can continue to use your plugin locally without ever publishing it.
When you go to Menu => Plugins => Development => Manage plugins in development you can click on the three dots of your plugin and press Publish. Also see Figma’s guide on publishing (opens in a new tab).
You can use the result of this guide or my figma-plugin-svelte-example (opens in a new tab) project as a template for any future Svelte Figma plugins.