Figma Component Variants: Property-Based State Management and Plugin API Control
How Figma Component Variants model state as named properties, how to switch them programmatically via the Plugin API's variantProperties, and how boolean properties prevent combinatorial explosion.
02 Aug 2026, 15:36 UTC

If your design system has a Button component copied into Button-Hover, Button-Disabled, Button-Large-Hover, and so on, you are maintaining a permutation matrix by hand. Component Variants collapse that matrix into one component set whose states are selected through named properties, and the Plugin API lets you switch those states programmatically. The useful takeaway: model states as properties, keep the variant count low with boolean properties, and drive state changes through variantProperties instead of instance swapping.
How variant properties actually work
A variant set is a single component (shown with a dashed purple outline on the canvas) that contains every permutation as children. Each child is named with a property syntax like Size=Large, State=Hover. When you place an instance, Figma parses those names and exposes the properties as dropdowns and toggles in the right-hand design panel. Changing a dropdown does not swap the instance for a different component; it re-points the instance at a different child of the same set, preserving overrides where layer names match.
This distinction matters for engineering decisions. Instance swapping across unrelated components breaks overrides and creates fragile links. Variant property changes stay inside a defined contract: the set of valid keys and values is declared once, in the component names.
Worked example: driving variants from a plugin
The Plugin API exposes the current state of an instance through variantProperties, a read-only-ish object you update by assigning a new object. The following script runs in a Figma plugin's code.ts (compiled to JavaScript, executed in Figma's plugin sandbox, not Node). It requires no special permissions beyond the plugin manifest's default "api": "1.0.0" declaration.
// code.ts — run in a Figma plugin sandbox
// Assumes the user has selected a variant instance, e.g. a Button set
// with properties Size={Small,Large} and State={Default,Hover,Disabled}
const node = figma.currentPage.selection[0];
if (!node || node.type !== "INSTANCE") {
figma.notify("Select a component instance first.");
figma.closePlugin();
} else {
const main = node.mainComponent;
if (!main || main.parent?.type !== "COMPONENT_SET") {
figma.notify("Instance is not part of a variant set.");
} else {
// Read valid values from the set before assigning
const defs = main.parent.componentPropertyDefinitions;
const wanted = { Size: "Large", State: "Disabled" };
for (const key of Object.keys(wanted)) {
if (!defs[key] || !defs[key].variantOptions?.includes(wanted[key])) {
figma.notify(`Invalid property: ${key}=${wanted[key]}`);
figma.closePlugin();
throw new Error("Invalid variant property");
}
}
node.setProperties(wanted);
figma.notify("Variant updated.");
}
figma.closePlugin();
}Two details in that script prevent the most common failure. First, componentPropertyDefinitions on the parent set tells you the valid keys and their variantOptions; assigning a value the set does not define throws. Second, use setProperties() rather than mutating the object in place — it validates and applies atomically. To verify the result, run the plugin from the Figma desktop app (Plugins → Development → your plugin) with a button instance selected, then confirm the property dropdowns in the design panel show the new values and the canvas rendering changed.
Controlling combinatorial explosion with boolean properties
A set with Size (3 values) × State (4 values) × Emphasis (2 values) is 24 variants. Add an icon toggle and you double to 48 by drawing near-duplicate frames. Boolean properties avoid this: instead of encoding Icon=True/False as separate variants, Figma lets a boolean property bind to layer visibility inside every variant. The variant count stays at 24, and the icon toggle appears as a switch in the panel.
The practical rule: if a property only shows or hides a layer, make it a boolean property, not a variant axis. Reserve variant axes for properties that genuinely change layout or styling, like size or interactive state. You can check your own exposure by counting children of the component set node — componentSet.children.length in the plugin console — and treating anything over roughly 50 as a performance risk worth measuring. Large sets increase file memory and can make property switching feel laggy on complex pages; test with your real components rather than assuming.
Limits and common mistakes
- Renaming property keys breaks instances. Changing
SizetoScalein the set orphans the mapping on existing instances, which fall back to default values. Rename through the library, publish, and accept updates in consuming files promptly — and verify a sample of instances afterward. - No nesting within the same set. A variant cannot contain another variant of its own set. Compose separate sets instead (e.g., an Icon set inside a Button set's variants works; Button inside Button does not).
- Overrides are matched by layer name. If your variants have inconsistent internal layer names, text overrides will not carry across property switches. Keep the layer tree identical across variants.
- API behavior varies by version. Property APIs (
setProperties,componentPropertyDefinitions) reflect the current Plugin API surface; check the manifestapiversion and the official changelog before shipping a plugin that depends on them, since this area has evolved.
The design decision in one line: variants are a contract, not a folder. Declare few axes, push visibility into booleans, and let plugins read the contract from componentPropertyDefinitions instead of hardcoding values.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.