Analytics for Figma widgets: what you can count and where the code goes
A widget runs only when someone clicks it and stops when the handler returns. What that means for users, sessions and events, with a working pattern for FigJam and Figma widgets.
, by Ilia
A widget looks like a plugin from the outside: same manifest, same figma global, same publish flow. It runs very differently, and the difference decides what you can count. This is what changes for analytics when the thing you built sits on the canvas instead of opening in a panel.
How a widget runs
A plugin has a clear life. The user starts it, it runs, the user closes it. A widget has no such moment. Figma's docs put it in one line: widgets run only in response to user interaction, and only on the client of the person who interacted. Someone clicks, Figma starts your code, runs the handler and terminates the widget when the handler is done. Everyone else in the file sees the result through multiplayer and runs nothing.
Three things follow from that.
- There is no open event and no session. A widget can sit in a FigJam board for a year. The only moments your code exists are the clicks.
- Every interaction is a fresh run. Anything you kept in a variable last time is gone, so an analytics client has to start, send and finish inside one handler.
- Ten people clicking the same widget are ten users, each counted from their own client. Nothing is double counted, because only the client that clicked runs the code.
Where a tracking call can go
Rendering is off limits. The function that returns your widget's layout runs synchronously and has to depend on the widget's state alone, so it cannot wait for a request. It also runs again on every state change, which means a call placed there counts renders and not people.
Event handlers are the place: onClick on a node, the callback of usePropertyMenu, a message from an iframe you opened. A handler may be async, and Figma keeps the widget alive until the promise it returns has resolved. That is the rule to remember. If you start a request and do not await it, the widget can be terminated before the request leaves.
The Plugin API is available inside a handler, including fetch and figma.currentUser. The manifest rules are the same as for a plugin: list the analytics host in networkAccess.allowedDomains, and ask for the currentuser permission if you want to count people and not clicks.
A widget with tode
The core package is the one a plugin uses. Initialise it inside the handler, track, and await both.
// code.tsx
import { tode } from '@tode-sdk/core';
const { widget } = figma;
const { AutoLayout, Text, useSyncedState } = widget;
async function track(name: string) {
await tode.init({ apiKey: 'your-api-key' });
await tode.trackAction(name);
}
function Counter() {
const [votes, setVotes] = useSyncedState('votes', 0);
return (
<AutoLayout
padding={16}
onClick={async () => {
setVotes(votes + 1);
await track('vote_cast');
}}
>
<Text>{votes} votes</Text>
</AutoLayout>
);
}
widget.register(Counter);"permissions": ["currentuser"],
"networkAccess": {
"allowedDomains": [
"https://processing.usetode.dev",
"wss://processing.usetode.dev"
]
}init reads the Figma user id, the editor and the widget id, and makes one request to resolve the country before the event is sent. Because each interaction is a new run, that happens on every tracked click: two requests instead of one. For a vote button or a menu choice nobody will notice. For something a user drags or clicks many times a second, track the outcome once and not every step.
Update the widget state first and track after, as in the example. The user sees the result immediately and the request finishes in the background of the same handler. The SDK never throws, so a failed request cannot break the click.
What you get, and what you do not
| Number | In a widget |
|---|---|
| Active users, new against returning, retention | Yes, from the Figma user id of whoever interacted |
| Actions by name and metrics | Yes, same calls as a plugin |
| Figma Design against FigJam | Yes, the editor is sent with every event |
| Country | Yes |
| Session length | Only while an iframe opened with figma.showUI is on screen |
| How many copies of the widget exist | No. A copy that nobody interacts with sends nothing |
| People who looked and did not click | No. Viewing runs no code at all |
The last two rows are the honest limit of widget analytics with any tool. You measure interaction. Reach stays a number on the Community page, with the same caveats as installs for a plugin.
If the widget opens a UI
Many widgets open an iframe for anything richer than a click: a text field, a settings form, a picker. From that moment the widget behaves like a plugin with a UI. The handler returns a promise that stays pending while the window is open, and the iframe can use one of the framework adapters the way a plugin UI does. tode measures the session there as the time the iframe stays connected, and actions tracked from the UI carry the same user id.
What to track in a widget
The same advice as for a plugin, with one shift. In a plugin the question is which feature people use. In a widget it is more often whether a second person touched it. A widget that one person inserts and nobody else clicks is a sticker. So name the actions after what a participant does (vote_cast, card_added, timer_started) and read the distinct users per action before the totals. The naming rules are in how to track usage.