Hey everyone!
I love what y’all are doing with widgets, it’s enabled me to build a really cool product for content modeling (Tapi) with a fraction of the work I’d have to put in to building my own infinite, collaborative canvas.
In this post, I’ll share my perceptions of how the widget API & DX could be better after working on it for almost 300h. Before I get into the nitty gritty of what I disliked, know that the amazing parts overshadow these. I’m a glad developer and have no regrets in choosing Figjam for my product 🙂
Issues and oddities with the widgets API
These are from my own personal experience and I haven’t done much digging to investigate them. I may be mistaken in an issue or two 🙂
There’s not much logic to the order here, I’m sorry about that 😅
SVGs render funkily
Strokes don’t render well, lines ends are joined, I think. Instead, I have to outline them & convert into fills first.
When using masks, any path that comes after the mask definition will only render if it uses the mask itself:
<svg
width="35"
height="35"
viewBox="0 0 35 35"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Renders -->
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M29.0514 14.1H25.9999V12.9H31.0999V18H29.8999V14.9486L20.9242 23.9243L20.0756 23.0758L29.0514 14.1ZM14.8999 14.9H23.9999V16.1H16.0999V27.9H27.8999V20H29.0999V29.1H14.8999V14.9Z"
fill="#262F3D"
/>
<mask
id="mask0_63_3255"
style="mask-type: alpha"
maskUnits="userSpaceOnUse"
x="4"
y="3"
width="20"
height="19"
>
<path d="M14 14V22H4.5V3.5H24V14H14Z" fill="black" />
</mask>
<g mask="url(#mask0_63_3255)">
<path
d="M6.5 17.5H20.5M6.5 9.5H20.5M5.5 13.5H21.5M13.5 21.5C13.5 21.5 9 19.5 9 13.5C9 7.5 13.5 5.5 13.5 5.5M13.5 5.5C13.5 5.5 18 7.5 18 13.5C18 19.5 13.5 21.5 13.5 21.5M13.5 5.5V21.5M21.5 13.5C21.5 17.9183 17.9183 21.5 13.5 21.5C9.08172 21.5 5.5 17.9183 5.5 13.5C5.5 9.08172 9.08172 5.5 13.5 5.5C17.9183 5.5 21.5 9.08172 21.5 13.5Z"
stroke="#262F3D"
stroke-width="1.2"
/>
</g>
<!-- Doesn't render -->
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M29.0514 14.1H25.9999V12.9H31.0999V18H29.8999V14.9486L20.9242 23.9243L20.0756 23.0758L29.0514 14.1ZM14.8999 14.9H23.9999V16.1H16.0999V27.9H27.8999V20H29.0999V29.1H14.8999V14.9Z"
fill="#262F3D"
/>
</svg>
Not deal breakers, but gets in the way of an optimal workflow of copying from Figma design & dropping into my widget codebase.
Confusing widgetId
WidgetNode.widgetId
(the global widget’s identification number in the Figma community) can get easily confused with figma.widget.useWidgetId()
(the individual widget node’s ID)
Even after 200h+ working on my widget, I’m getting confused with all of the different IDs this brings to the table
Widgets change their widgetId
when updated
When users update widgets to the latest version, Figma deletes the current one and creates a new instance for the new version, with a new ID. For reference, I’m talking about the below:
For projects using node IDs in their code for referencing across widgets, this behavior breaks the upgrade path. I tried creating my own workaround for this (which is super messy), but it’s hard to test and leads to all sorts of spaghetti logic in useEffects 😬
Also, it’d be great if users could select multiple widgets and update all at once. Having to do so individually is tedious and makes it harder for existing users to adopt new versions.
(accessibility) Widget iframe keyboard focus & tabbing
If they’re editing a form and accidentally tab too much, they can’t go back into the frame with their keyboard because tabs work differently when the main canvas is focused. Ideally, Figma would trap tabs like we do in regular web dialogs (see the dialog element’s behavior).
Also, when opening a frame, Figjam could focus on the close button or an interactive element to make keyboard usage easier.
I saw these get in the way of users a couple of times during user interviews.
Publishing workflow gets in the way of businesses
Widgets must be published to the entire community in order to be accessed outside developers’ machines.
As a result, I had to make Tapi public in order to conduct user interviews, and now I can’t easily run feature betas and unlock specific behavior for specific users.
(plugin API) Creating styled & labeled connectors is hard
I have to load the fonts every time I use them, even when it’s Figma’s default font, Inter. And editing text is a pain:
- you can’t use hex colors;
- RGB isn’t base 255, it’s 1-0;
- you need to specify the range of your fill and can’t apply them text-wide;
- it isn’t clear what weights are available for which fonts
The result is a lot of verbosity to create a simple connector label:
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' })
connector.text.fontName = {
family: 'Inter',
style: 'Bold',
}
connector.text.textCase = 'UPPER'
connector.text.letterSpacing = { unit: 'PIXELS', value: 0.5 }
connector.text.characters = typeStyle.label
connector.text.setRangeFills(0, connector.text.characters.length, o
// Label in gray/800
{ type: 'SOLID', color: typeStyle.labelColor, opacity: 0.75 },
])
It’s impossible to set other widget’s data
In my widget, I have tons of features that require performing document-wide changes to all instances. However, widgetSyncedState
is read-only, making it impossible to set other widget’s data.
I could set global plugin data with widgetNode.setPluginData
and sync the individual widget once the user interacts with it (through useEffect). That’s complex to manage reliably, though, and leads to terrible UX as users won’t get immediate feedback and will need to interact with the widget for it to pick up the changes.
I could duplicate the node with a new state and delete the old, but then I’d need to update all references, which would require re-creating ALL widget instances. Imagine the performance and sync issues that arise from this 😥
Creating a new widget is unnecessarily hard
The widgetNode.cloneWidget
method requires you to erase all existing data from cloned widget if you want to start from scratch. This would be okay if it was easy to clean state keys in syncedStateOverrides
, which doesn’t support null
& undefined
as values.
For textual/numeric state values, you pass an empty string or 0 - easy enough, although a bit bothersome.
But for synced maps, you can’t just pass an empty object {}
else Figma will ignore it and instantiate the widget with the cloned node’s map state. Instead, you must pass an object with at least a single non-null property to it.
So to allow my widgets to create siblings, I had to add an invalid-signature
property to the synced maps and run a clean-up function in the useEffect
of that widget. The logic is kinda gross and hard to track, to be honest. Here’s a portion of it (missing the useEffect bit)
const newWidget = clonedWidget.cloneWidget(
// (syncedStateOverrides)
// Unfortunately we can't pass null or undefined to syncedStateOverrides, so we must manually overwrite it
{
label: startingModel.label || '',
icon: startingModel.icon || ('' as any),
modelId: startingModel.modelId || '',
initializedAs: startingModel.initializedAs || {},
nodeUid: newNodeUid,
quantity: startingModel.quantity || ModelQuantity.collection,
type: startingModel.type || ModelType.document,
} as ModelConfig,
// (syncedMapOverrides)
{
// Reset all attribute maps
...Object.keys(clonedWidget.widgetSyncedState)
.filter((key) => key.startsWith('attribute'))
.reduce((acc, key) => {
return {
...acc,
ckey]:
key === 'attributes'
? {
// For a reason I couldn't figure out, you can't set an empty object for the attributes synced map
// The widget gets rendered, but you can't interact with it
// The error: "Attempt to set a `nullopt` value on MultiplayerMap" in setWidgetSyncedState"
oINVALID_SIGNATURE_KEY]: {
invalid: true,
},
}
: {},
}
}, {}),
// And add the starting attributes, if applicable
...startingAttributeDatas,
},
)
Finally, placing the widget is kinda hard as you have to calculate a position where it won’t be overlapped 🤔
Hard to do iframe window size management
I feel weird about setting a fixed pixel-based width & height because users’s OS zoom level varies. Apparently there’s no way to make them responsive to their content, and I can’t get the available screen size to size the frame accordingly.
As a result, I have to make all frames quite tiny to make room for zoomed-in and tiny monitors like mine.
The widget-app-template
It doesn’t teach us how to re-use code across UI & widget, which is essential.
Also, it could feature a multi-file boilerplate for the widget code. I initially thought you couldn’t even split that code.tsx
file and was stuck trying to manage 900+ lines of a lot of logic. Now, just my widget has way over 3000 LOC spread across multiple files, I couldn’t do it in a single one.
You can’t access plugin data in the render method
As you can’t run the figma
plugin API from the render method of widgets, you can’t access global plugin data, only widget-specific state.
This makes it super hard to configure UIs to respond to project configuration. Some examples of what I’d like to do with Tapi:
- Offer users the possibility to rename the labels in the widget according to their role (designers, content people and developers have different names for the same things)
- Split the content modeling workflow in multiple stages, and have all widgets reflect the currently selected stage - in the strategic stage, only models’ names would be available, for example.
- If they have a paid project, display access to the models store
I’d also love to show other widget instances’s data to allow quick actions like “connect to X”.
No way to expose actions on non-widget nodes
For example, I’d love to be able to offer users the ability to import a content model from selected sticky notes. Apparently, only traditional plugins have this right, and I’d need to create a separate plugin just to do that. As I don’t think forcing users to install 2 separate entities, and as I don’t want to go through two separate publishing processes, this is a feature I simply can’t build.
Would be lovely to have connection hooks
Stickable hooks are pretty cool and I’m looking for inspiration on how to use them, but I know for sure I’d use a hook that fires when the widget is connected to some other node.
In Tapi, it’d make content modeling much faster & intuitive as I’d be able to auto-create new reference attributes when users manually connect a model to another. Ideally, the hook would fire on both widgets being connected.
Conclusion
You’re off to a great start, and additions like the amazing Input give me hope you’re on the right track. A huge thank you to the whole team behind widgets & Figma in general!
The issues above don’t keep me from building something really compelling, and some forced me to get really creative about my implementation, which was fun.
If my product succeeds, in the long term I’ll be forced to leave Figjam, though. I’m very limited by the architecture of splitting widget & iframe code, and having access to plugin data only upon user interaction, tied to the canvas-only UI of widgets.
BUT I’m sure you’re aware of most of these and are making a conscious choice on what constitutes the ideal Figjam widget. I may be trying to overshoot, that’s all 🙂