Figma Variables API Guide for Plugin Developers

JP
Jordan Pham
UX/UI Designer & Plugin Developer | 7+ Years Experience

Figma Variables are typed values — color, number, string, or boolean — stored in a collection, organized into modes, and bound to node properties so that a single variable can drive different resolved values depending on context (light versus dark mode, for instance, or a locale switch). Unlike styles, a variable’s value isn’t fixed; it’s resolved at read time against whichever mode is currently active. For plugin developers, that resolution step is where most of the confusion — and most of the bugs — actually originate.

This guide works through the API in a troubleshooting format: a symptom you’re likely to hit, the underlying cause, and the fix. If you’re building a plugin that reads, writes, or syncs variables, treat this as a reference to come back to when something isn’t behaving the way you expected.


Symptom: getVariableByIdAsync Returns null for a Variable You Know Exists

Cause: You’re calling the synchronous getVariableById (deprecated in newer API versions) or you’re querying an ID from a different file without the correct library/team permissions. Variables that live in a published library aren’t automatically visible to every plugin call — they need to be imported into the current file’s scope first, similar to how component instances work.

Fix: Use figma.variables.getVariableByIdAsync(id) and await it properly. If the variable belongs to a team library, call figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync() first to confirm the collection is actually available to the current file, then import the specific variables you need with figma.variables.importVariableByKeyAsync(key). Skipping the import step is the single most common reason a valid variable ID resolves to nothing.


Symptom: Bound Values Look Correct in the UI but Wrong When Read via the API

Cause: A node property bound to a variable doesn’t store the resolved value directly — it stores a reference (VariableAlias) pointing at a variable ID. If your plugin reads node.fills or node.cornerRadius expecting a literal number or color, you’ll get an alias object instead whenever a binding exists, and naive code that assumes a plain value will silently misbehave or throw.

Fix: Check for a binding before assuming a literal value. Use node.boundVariables to see which properties carry variable references, and resolve the actual value with variable.valuesByMode[modeId] for the mode you care about. Never assume node.fills[0].color is a static object — verify node.boundVariables?.fills first.


Symptom: The Same Variable Shows Different Values in Different Frames

Cause: This is expected behavior, not a bug — but it trips up developers who think of variables as single-valued, the way constants work in most programming languages. A variable’s resolved value depends on which mode is active for the node’s context, and mode selection can be set at the page level, frame level, or explicitly overridden on a specific node.

Fix: Before reading a variable’s value, determine the effective mode for the node in question. Walk up from the node to find the nearest ancestor with an explicit mode set via node.resolvedVariableModes, rather than assuming the collection’s default mode applies everywhere. If your plugin needs to display “the” value of a variable in a UI panel, be explicit about which mode you’re showing, and label it accordingly so users aren’t misled into thinking it’s the only value.


Symptom: setValueForMode() Throws or Silently Fails

Cause: Two common causes here. First, the mode ID you’re passing doesn’t belong to the variable’s collection — mode IDs are scoped per collection, not global, so reusing a mode ID from a different collection fails. Second, you’re attempting to set a value whose type doesn’t match the variable’s declared resolvedType (trying to write a string into a variable typed as COLOR, for example).

Fix: Fetch the variable’s collection with figma.variables.getVariableCollectionByIdAsync(variable.variableCollectionId) and confirm the mode ID exists in collection.modes before writing. Match the value’s shape precisely to resolvedType: COLOR expects an RGBA object with r, g, b, a all normalized between 0 and 1 — not a hex string, not a CSS-style rgb() value.


Symptom: Creating a New Variable Collection Doesn’t Show Up for Collaborators

Cause: Variable collections created locally within a file are file-scoped by default. If the collection hasn’t been published as part of a library, other files — and other collaborators working in separate files — won’t see it, regardless of team permissions.

Fix: This isn’t something your plugin code controls directly; publishing a library is a user action taken through Figma’s library publishing UI. What your plugin can do is create the collection correctly so it’s ready to publish — set collection.hiddenFromPublishing = false where relevant, and make sure variable names and descriptions are populated, since these carry over into the published library and are what teammates will actually see when browsing available variables.


Symptom: Bulk Variable Creation Runs Extremely Slowly

Cause: Calling figma.variables.createVariable() and then immediately calling setValueForMode() for each individual variable, one at a time, in a loop with no batching, generates a large number of discrete document operations. Figma’s document model isn’t optimized for thousands of tiny sequential writes triggered this way, and performance degrades roughly linearly with variable count once you’re past a few hundred.

Fix: Where possible, group creation logic so that related writes happen in tight succession rather than being interleaved with other plugin work (UI updates, network calls, and so on) between each variable. If you’re importing a large design token set, consider chunking the work with periodic yields (await new Promise(r => setTimeout(r, 0))) so Figma’s renderer stays responsive between batches, rather than blocking the main thread for the entire operation.


Symptom: Variable Aliases Point to Other Variables and Your Resolver Breaks

Cause: Variables can reference other variables as their value in a given mode — a semantic color variable pointing at a primitive color variable, for example. If your plugin’s resolution logic reads valuesByMode[modeId] and assumes it always gets a terminal value, an alias-to-variable reference will break that assumption immediately.

Fix: Write your resolver recursively. Check whether the returned value is itself a VariableAlias object (it will have an id field rather than a direct color/number/string/boolean), and if so, fetch that variable and resolve again against the appropriate mode, continuing until you hit a literal value. Guard against circular references with a depth limit or visited-set check — Figma’s UI prevents users from creating true cycles, but defensive code costs little and saves you from an infinite loop if that guarantee is ever violated.


Symptom: Plugin UI Shows Stale Variable Values After a User Edits Them Elsewhere

Cause: Your plugin cached variable data once at startup and never re-subscribed to changes. Variable collections can be edited outside your plugin’s direct control — by the user through Figma’s native variables panel, or by another plugin running concurrently.

Fix: Register a documentchange listener via figma.on('documentchange', callback) and inspect change events for entries related to variable nodes, or re-fetch relevant variables when your plugin’s UI regains focus. Don’t rely on a single fetch-at-launch pattern for any plugin that stays open for an extended editing session.


A Reference Table for Common Variables API Calls

TaskMethodCommon Pitfall
Fetch a variable by IDgetVariableByIdAsyncForgetting to await, or using deprecated sync version
Import a library variableimportVariableByKeyAsyncSkipping this before referencing an external variable
Read a bound propertynode.boundVariablesAssuming a literal value instead of checking for an alias
Determine active modenode.resolvedVariableModesAssuming one global mode applies everywhere
Write a mode valuesetValueForModeMismatched mode ID or wrong value type for resolvedType
Resolve aliased valuesrecursive valuesByMode lookupNot handling variable-to-variable references

Before You Ship: A Short Verification Checklist

Run through this list before releasing a plugin feature that touches variables:

  • Does your code check node.boundVariables before treating any property as a plain literal?
  • Have you tested against a file with multiple modes active, not just the default?
  • Does bulk creation or updating avoid blocking the main thread for large token sets?
  • Are you handling variable-to-variable aliasing, or will your resolver break on a semantic-to-primitive reference chain?
  • Does your plugin refresh variable data during long editing sessions rather than caching once at launch?

Most variable-related bugs that reach production trace back to one of these five items rather than to some obscure API edge case. Getting the fundamentals — binding checks, mode resolution, and alias handling — solid first will save considerably more debugging time than chasing rarer failure modes prematurely.

Which part of the Variables API is currently giving your plugin the most trouble — mode resolution, alias handling, or something with library publishing? Describe the specific behavior you’re seeing and I can help narrow down the likely cause.

About the Author

Jordan Pham is a UX/UI designer and Figma plugin developer with 7 years of design experience and several published plugins on the Figma Community, used by thousands of designers.