Open-Source Meets Design Tooling With Penpot

About The Author

Atila Fassina is a Google Dev Expert, member of the Solid DX team, and a Tauri advocate. He enjoys making complex code simpler via articles, conference talks, … More about Atila ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

Penpot helps designers and developers work better together by offering a free, open-source design tool based on open web standards. Today, let’s explore its newly released Penpot Plugin System. So now, if there’s a functionality missing, you don’t need to jump into the code base straight away; you can create a plugin to achieve what you need. And you can even serve it from localhost!

Penpot is a free, open-source design tool that allows true collaboration between designers and developers. Designers can create interactive prototypes and design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast because it’s built with web technologies, works in the browser, and has already passed 33K starts on GitHub.

The UI feels intuitive and makes it easy to get things done, even for someone who’s not a designer (guilty as charged!). You can get things done in the same way and with the same quality as with other more popular and closed-source tools like Figma.

Penpot tool
(Large preview)

Why Open-Source Is Important

As someone who works with commercial open-source on my day-to-day, I strongly believe in it as a way to be closer to your users and unlock the next level of delivery. Being open-source creates a whole new level of accountability and flexibility for a tool.

Developers are a different breed of user. When we hit a quirk or a gap in the UX, our first instinct is to play detective and figure out why that pattern stuck out as a sore thumb to what we’ve been doing. When the code is open-source, it’s not unusual for us to jump into the source and create an issue with a proposal on how to solve it already. At least, that’s the dream.

On top of that, being open-source allows you and your team to self-host, giving you that extra layer of privacy and control, or at least a more cost-effective solution if you have the time and skills to DYI it all.

When the cards are played right, and the team is able to afford the long-term benefits, commercial open-source is a win-win strategy.

Introducing: Penpot Plugin System

Talking about the extensibility of open-source, Penpot has the PenpotHub the home for open-source templates and the newly released plugin gallery. So now, if there’s a functionality missing, you don’t need to jump into the code-base straightaway — you can create a plugin to achieve what you need. And you can even serve it from localhost!

Creating Penpot Plugins

When it comes to the plugins, creating one is extremely ergonomic. First, there are already set templates for a few frameworks, and I created one for SolidJS in this PR — the power of open-source!

When using Vite, plugins are Single-Page Applications; if you have ever built a Hello World app with Vite, you have what it takes to create a plugin. On top of that, the Penpot team has a few packages that can give you a headstart in the process:

npm install @penpot/plugin-styles

That will allow you to import with a CSS loader or a CSS import from @penpot/plugin-styles/styles.css. The JavaScript API is available through the window object, but if your plugin is in TypeScript, you need to teach it:

npm add -D @penpot/plugin-types

With those types in your node_modules, you can pop-up the tsconfig.json and add the types to the compilerOptions.

{
  "compilerOptions": {
    "types": ["@penpot/plugin-types"]
  }
}

And there you are, now, the Language Service Provider in your editor and the TypeScript Compiler will accept that penpot is a valid namespace, and you’ll have auto-completion for the Penpot APIs throughout your entire project. For example, defining your plugin will look like the following:

penpot.ui.open("Your Plugin Name", "", {
  width: 500,
  height: 600
})

The last step is to define a plugin manifest in a manifest.json file and make sure it’s in the outpot directory from Vite. The manifest will indicate where each asset is and what permissions your plugin requires to work:

{
  "name": "Your Plugin Name",
  "description": "A Super plugin that will win Penpot Plugin Contest",
  "code": "/plugin.js",
  "icon": "/icon.png",
  "permissions": [
    "content:read",
    "content:write",
    "library:read",
    "library:write",
    "user:read",
    "comment:read",
    "comment:write",
    "allow:downloads"
  ]
}

Once the initial setup is done, the communication between the Penpot API and the plugin interface is done with a bidirectional messaging system, not so different than what you’d do with a Web-Worker.

So, to send a message from your plugin to the Penpot API, you can do the following:

penpot.ui.sendMessage("Hello from my Plugin");

And to receive it back, you need to add an event listener to the window object (the top-level scope) of your plugin:

window.addEventListener("message", event => {
  console.log("Received from Pendpot::: ", event.data);
})

A quick performance tip: If you’re creating a more complex plugin with different views and perhaps even routes, you need to have a cleanup logic. Most frameworks provide decent ergonomics to do that; for example, React does it via their return statements.

useEffect(() => {
  function handleMessage(e) {
    console.log("Received from Pendpot::: ", event.data);
  }
  window.addEventListener('message', handleMessage);
  
  return () => window.removeEventListener('message', handleMessage);
}, []);

And Solid has onMount and onCleanup helpers for it:

onMount(() => {
  function handleMessage(e) {
    console.log("Received from Penpot::: ", event.data);
  }
  window.addEventListener('message', handleMessage);
})

onCleanup(() => {
  window.removeEventListener('message', handleMessage);
})

Or with the @solid-primitive/event-listener helper library, so it will be automatically disposed:

import { makeEventListener } from "@solid-primitives/event-listener";

function Component() {
  
  const clear = makeEventListener(window, "message", handleMessage);
  
  // ...
  return (<span>Hello!</span>)
}

In the official documentation, there’s a step-by-step guide that will walk you through the process of creating, testing, and publishing your plugin. It will even help you out.

So, what are you waiting for?

Plugin Contest: Imagine, Build, Win

Well, maybe you’re waiting for a push of motivation. The Penpot team thought of that, which is why they’re starting a Plugin Contest!

For this contest, they want a fully functional plugin; it must be open-source and include comprehensive documentation. Detailing its features, installation, and usage. The first prize is US$ 1000, and the criteria are innovation, functionality, usability, performance, and code quality. The contest will run from November 15th to December 15th.

Final Thoughts

If you decide to build a plugin, I’d love to know what you’re building and what stack you chose. Please let me know in the comments below or on BlueSky!

Smashing Editorial (yk)