Widgets
Widgets Overview
Build interactive canvas objects with the Figma Widget API — a React-like framework for Figma.
Widgets are interactive objects that live directly on the Figma or FigJam canvas. Unlike plugins (which run as modal tools), widgets are persistent, collaborative objects that multiple users can interact with simultaneously.
What Makes Widgets Special
Canvas-Native
Widgets render as first-class canvas objects. Users can move, resize, and interact with them like any other design element.
Collaborative
Widget state syncs across all users in real-time. Multiple people can interact with the same widget simultaneously.
React-Like API
Build widgets using a familiar JSX syntax with hooks like useSyncedState, usePropertyMenu, and useEffect.
Persistent State
Widget data persists in the file. State survives page reloads and is included when the file is duplicated or versioned.
Widget Lifecycle
A widget goes through several phases:
- Registration — The widget function is registered with
widget.register() - Render — Figma calls your widget function to get the UI tree
- Interaction — User clicks trigger state updates
- Re-render — State changes trigger a new render cycle
Building a Widget
Widgets use a React-like API with special Figma primitives:
const { widget } = figma;
const { Text, AutoLayout, useSyncedState, usePropertyMenu } = widget;
function CounterWidget() {
const [count, setCount] = useSyncedState("count", 0);
usePropertyMenu(
[
{
itemType: "action",
propertyName: "reset",
tooltip: "Reset counter",
},
],
({ propertyName }) => {
if (propertyName === "reset") setCount(0);
}
);
return (
<AutoLayout
direction="horizontal"
spacing={12}
padding={16}
cornerRadius={12}
fill="#FFFFFF"
stroke="#E5E5E5"
>
<AutoLayout
onClick={() => setCount(count - 1)}
padding={{ horizontal: 12, vertical: 8 }}
cornerRadius={8}
fill="#F3F0FF"
hoverStyle={{ fill: "#E8E0F0" }}
>
<Text fontSize={16} fontWeight={600}>−</Text>
</AutoLayout>
<Text fontSize={24} fontWeight={700} width={60} horizontalAlignText="center">
{String(count)}
</Text>
<AutoLayout
onClick={() => setCount(count + 1)}
padding={{ horizontal: 12, vertical: 8 }}
cornerRadius={8}
fill="#A259FF"
hoverStyle={{ fill: "#B370FF" }}
>
<Text fontSize={16} fontWeight={600} fill="#FFFFFF">+</Text>
</AutoLayout>
</AutoLayout>
);
}
widget.register(CounterWidget);
Available Primitives
| Primitive | Description |
|---|---|
AutoLayout | Flexible container with auto-layout (like CSS flexbox) |
Frame | Fixed-size container |
Text | Text node with font, size, and color controls |
SVG | Inline SVG rendering |
Image | Image display from URL or bytes |
Input | Text input field |
Ellipse | Ellipse shape |
Rectangle | Rectangle shape |
Line | Line shape |
Key Hooks
| Hook | Purpose |
|---|---|
useSyncedState(key, default) | State that syncs across all users |
useSyncedMap(key) | Key-value map that syncs across users |
usePropertyMenu(items, handler) | Right-click property menu |
useEffect(fn) | Side effects on mount |
useWidgetNodeId() | Get the widget’s node ID |