# Component & Variant API Patterns > Part of the [use_figma skill](../SKILL.md). How to correctly use the Plugin API for components, variants, and component properties. > > For design system context (when to use variants vs properties, code-to-Figma translation, property model), see [wwds-components](working-with-design-systems/wwds-components.md). ## Contents - Creating a Component - Combining Components into a Component Set (Variants) - Laying Out Variants After combineAsVariants (Required) - Component Properties: addComponentProperty API - Linking Properties to Child Nodes (Required) - INSTANCE_SWAP: Avoiding Variant Explosion - Discovering Existing Conventions in the File - Importing Components by Key - Working with Instances (finding variants, setProperties, text overrides, detachInstance) ## Creating a Component `figma.createComponent()` returns a `ComponentNode`, which behaves like a `FrameNode` but can be published, instanced, and combined into variant sets. ```javascript const comp = figma.createComponent(); comp.name = "MyComponent"; comp.layoutMode = "HORIZONTAL"; comp.primaryAxisAlignItems = "CENTER"; comp.counterAxisAlignItems = "CENTER"; comp.paddingLeft = 12; comp.paddingRight = 12; comp.layoutSizingHorizontal = "HUG"; comp.layoutSizingVertical = "HUG"; comp.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 } }]; ``` ## Combining Components into a Component Set (Variants) `figma.combineAsVariants(components, parent)` takes an array of `ComponentNode`s (not frames — frames will throw) and groups them into a `ComponentSetNode`. Variant names use a `Property=Value` format. Every unique combination must exist as a child component — missing ones show as blank gaps in the variant picker. ```javascript // Each component's name encodes its variant properties const comp1 = figma.createComponent(); comp1.name = "size=md, style=primary"; const comp2 = figma.createComponent(); comp2.name = "size=md, style=secondary"; const componentSet = figma.combineAsVariants([comp1, comp2], figma.currentPage); componentSet.name = "Button"; ``` **Before creating variants, inspect the file** for existing naming patterns. Different files use different conventions (`State=Default` vs `state=default` vs `State/Default`). Always match what's already there. ## Laying Out Variants After combineAsVariants (Required) After `combineAsVariants`, all children stack at `(0, 0)`. You **must** position them or the component set will appear as a single collapsed element with all variants overlapping. ```javascript const cs = figma.combineAsVariants(components, figma.currentPage); // Simple row layout cs.children.forEach((child, i) => { child.x = i * 150; child.y = 0; }); // CRITICAL: resize the component set from actual child bounds let maxX = 0, maxY = 0; for (const child of cs.children) { maxX = Math.max(maxX, child.x + child.width); maxY = Math.max(maxY, child.y + child.height); } cs.resizeWithoutConstraints(maxX + 40, maxY + 40); ``` For multi-axis variants (e.g., size × style × state), parse the child's name to determine grid position: ```javascript for (const child of cs.children) { const props = Object.fromEntries( child.name.split(', ').map(p => p.split('=')) ); const col = stateValues.indexOf(props.state); const row = styleValues.indexOf(props.style); child.x = col * colWidth; child.y = row * rowHeight; } ``` ## Component Properties: addComponentProperty API `addComponentProperty` adds a TEXT, BOOLEAN, or INSTANCE_SWAP property to a component. It returns a **string key** (e.g., `"label#4:0"`) — never hardcode or guess this key. ```javascript // Returns the key as a string — capture it! const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Default text'); const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true); const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComponentId); ``` **Timing**: Add component properties to each variant component **before** calling `combineAsVariants`. After combining, the component set inherits all properties from its children. Do not add properties to the `ComponentSetNode` directly. ## Linking Properties to Child Nodes (Required) A property that is added but not linked to a child node does **nothing**. You must set `componentPropertyReferences` on the child: ```javascript // TEXT property → link to a text node's characters const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button'); const textNode = figma.createText(); textNode.characters = "Button"; comp.appendChild(textNode); textNode.componentPropertyReferences = { characters: labelKey }; // BOOLEAN + INSTANCE_SWAP → link to an instance node const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true); const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id); const iconInstance = iconComp.createInstance(); comp.appendChild(iconInstance); iconInstance.componentPropertyReferences = { visible: showIconKey, // BOOLEAN controls show/hide mainComponent: iconSlotKey // INSTANCE_SWAP controls which component }; ``` **Valid `componentPropertyReferences` keys:** - `characters` — TEXT property on a TextNode - `visible` — BOOLEAN property (any node) - `mainComponent` — INSTANCE_SWAP property on an InstanceNode ## INSTANCE_SWAP: Avoiding Variant Explosion When a component has many possible sub-elements (e.g., 30 different icons), **never** create a variant per sub-element. Use a single INSTANCE_SWAP property instead — the user picks from any compatible component at design time. ```javascript // Create icon as its own ComponentNode const iconComp = figma.createComponent(); iconComp.name = "Icon/Search"; iconComp.resize(24, 24); const svgNode = figma.createNodeFromSvg('...'); iconComp.appendChild(svgNode); // Use it as the default for INSTANCE_SWAP const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id); const instance = iconComp.createInstance(); comp.appendChild(instance); instance.componentPropertyReferences = { mainComponent: iconSlotKey }; ``` This works for icons, avatars, badges, or any swappable nested element. ## Discovering Existing Conventions in the File **Always inspect the file before creating components.** Different files have different naming styles, structures, and conventions. Your code should match what's already there. ### List all existing components across all pages ```javascript const results = []; for (const page of figma.root.children) { await figma.setCurrentPageAsync(page); page.findAll(n => { if (n.type === 'COMPONENT') results.push(`[${page.name}] ${n.name} (COMPONENT) id=${n.id}`); if (n.type === 'COMPONENT_SET') results.push(`[${page.name}] ${n.name} (COMPONENT_SET) id=${n.id}`); return false; }); } return results.join('\n'); ``` ### Inspect an existing component set's variant naming pattern ```javascript const cs = await figma.getNodeByIdAsync('COMPONENT_SET_ID'); const variantNames = cs.children.map(c => c.name); const propDefs = cs.componentPropertyDefinitions; return { variantNames, propDefs }; ``` ### Find existing components in the file ```javascript const components = []; for (const page of figma.root.children) { await figma.setCurrentPageAsync(page); page.findAll(n => { if (n.type === 'COMPONENT') { components.push({ name: n.name, id: n.id, page: page.name, w: n.width, h: n.height }); } return false; }); } return components; ``` ## Importing Components by Key (Team Libraries) `importComponentByKeyAsync` and `importComponentSetByKeyAsync` import components from **team libraries** (not the same file you're working in). For components in the current file, use `figma.getNodeByIdAsync()` or `findOne()`/`findAll()` to locate them directly. ```javascript // Import a component from a team library const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY"); const instance = comp.createInstance(); // Import a component set from a team library and pick a variant const set = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY"); const variant = set.children.find(c => c.type === "COMPONENT" && c.name.includes("size=md") ) || set.defaultVariant; const variantInstance = variant.createInstance(); ``` ## Working with Instances ### Finding the right variant in a component set Parse variant names to match on multiple properties simultaneously: ```javascript const compSet = await figma.importComponentSetByKeyAsync("KEY"); const variant = compSet.children.find(c => { const props = Object.fromEntries( c.name.split(', ').map(p => p.split('=')) ); return props.variant === "primary" && props.size === "md"; }) || compSet.defaultVariant; const instance = variant.createInstance(); ``` ### Setting variant properties on an instance After creating an instance from a component set, you can set variant properties via `setProperties`: ```javascript const instance = defaultVariant.createInstance(); instance.setProperties({ "variant": "primary", "size": "medium" }); ``` ### Overriding text in a component instance **Always discover component properties BEFORE writing text overrides.** Components expose text as `TEXT`-type component properties, and `setProperties()` is the correct way to override them. Direct `node.characters` changes on property-managed text may be overridden by the component property system on render. **Step 1: Inspect componentProperties on a sample instance:** ```javascript const instance = comp.createInstance(); const propDefs = instance.componentProperties; // Returns e.g.: { "Label#2:0": { type: "TEXT", value: "Button" }, "Has Icon#4:64": { type: "BOOLEAN", value: true } } return propDefs; ``` Also check nested instances — a parent component may not expose text properties directly, but its nested child instances might: ```javascript const nestedInstances = instance.findAll(n => n.type === "INSTANCE"); const nestedProps = nestedInstances.map(ni => ({ name: ni.name, id: ni.id, properties: ni.componentProperties })); ``` **Step 2: Use setProperties() for TEXT-type properties:** ```javascript const instance = comp.createInstance(); const propDefs = instance.componentProperties; for (const [key, def] of Object.entries(propDefs)) { if (def.type === "TEXT") { instance.setProperties({ [key]: "New text value" }); } } ``` For nested instances that expose their own TEXT properties, call `setProperties()` on the nested instance: ```javascript const nestedHeading = instance.findOne(n => n.type === "INSTANCE" && n.name === "Text Heading"); if (nestedHeading) { nestedHeading.setProperties({ "Text#2104:5": "Actual heading text" }); } ``` **Step 3: Only fall back to direct node.characters for unmanaged text.** If text is NOT controlled by any component property, find text nodes directly. **Always load the node's actual font first** — instance text nodes inherit fonts from the source component, so don't assume Inter Regular: ```javascript const textNodes = instance.findAll(n => n.type === "TEXT"); for (const t of textNodes) { await figma.loadFontAsync(t.fontName); t.characters = "Updated text"; } ``` ### detachInstance() invalidates ancestor node IDs **Warning:** When `detachInstance()` is called on a nested instance inside a library component instance, the parent instance may also get implicitly detached (converted from INSTANCE to FRAME with a **new ID**). Subsequent `getNodeByIdAsync(oldParentId)` returns null. ```javascript // WRONG — cached parent ID becomes invalid after child detach const parentId = parentInstance.id; nestedChild.detachInstance(); const parent = await figma.getNodeByIdAsync(parentId); // null! // CORRECT — re-discover nodes by traversal from a stable (non-instance) parent const stableFrame = await figma.getNodeByIdAsync(manualFrameId); // a frame YOU created nestedChild.detachInstance(); // Re-find the parent by traversing from the stable frame const parent = stableFrame.findOne(n => n.name === "ParentName"); ``` If you must detach multiple nested instances across sibling components, do it in a **single** `use_figma` call — discover all targets by traversal at the start before any detachment mutates the tree. ## Inspecting Component Metadata (Deep Traversal) These helpers extract the full property schema and descendant structure of a component. Useful for understanding complex components before creating instances or setting properties. ```javascript /** * Imports a component or component set from a library by its published key. * Tries COMPONENT first, then falls back to COMPONENT_SET. * * @param {string} componentKey - The published key of the component or component set. * @returns {Promise} */ async function importComponentByKey(componentKey) { try { return await figma.importComponentByKeyAsync(componentKey); } catch { try { return await figma.importComponentSetByKeyAsync(componentKey); } catch { throw new Error(`No Component or Component Set available with key '${componentKey}'`); } } } /** * Given a main component node, returns the component set parent if one exists, * otherwise returns the component itself. Used to get the top-level node that * holds `componentPropertyDefinitions`. * * @param {ComponentNode} mainComponent * @returns {ComponentNode|ComponentSetNode} */ function getRelevantComponentNode(mainComponent) { return mainComponent.parent.type === "COMPONENT_SET" ? mainComponent.parent : mainComponent; } /** * Extracts `componentPropertyDefinitions` from a component or component set node * into a flat map keyed by property key. * * @param {ComponentNode|ComponentSetNode} node * @returns {Record} */ function getComponentProps(node) { const result = {}; for (let key in node.componentPropertyDefinitions) { const prop = { name: key.replace(/#[^#]+$/, ""), type: node.componentPropertyDefinitions[key].type, key: key }; if (prop.type === "VARIANT") { prop.variantOptions = node.componentPropertyDefinitions[key].variantOptions; } result[key] = prop; } return result; } /** * Recursively walks a component tree and collects all INSTANCE and TEXT nodes * into `result`, keyed by `TYPE[name]`. Handles variant namespacing and * deduplicates nodes with identical names but differing property references. * * @param {SceneNode} node - The node to traverse. * @param {string[]} namespace - Accumulated variant names for the current path. * @param {Record} result - Accumulator object populated in place. */ function collectDescendants(node, namespace, result) { if (node.type === "INSTANCE" || node.type === "TEXT") { const references = node.componentPropertyReferences || {}; if (!node.visible && !references.visible) return; const object = { type: node.type, name: node.name, references }; let key = `${node.type}[${node.name}]`; if (result[key] && JSON.stringify(references) !== JSON.stringify(result[key].references)) { key += btoa(btoa(unescape(encodeURIComponent(JSON.stringify(references))))); } if (node.type === "INSTANCE") { const mainComponent = getRelevantComponentNode(node.mainComponent); object.properties = getComponentProps(mainComponent); object.descendants = {}; object.mainComponentName = mainComponent.name; collectDescendants(mainComponent, [], object.descendants); } const start = namespace.length ? { variants: [] } : {}; result[key] = Object.assign(object, result[key] || start); if (namespace.length) result[key].variants.push(namespace[namespace.length - 1]); } else if ("children" in node && node.visible) { if (node.type === "COMPONENT" && node.parent.type === "COMPONENT_SET") namespace.push(node.name); node.children.forEach(child => collectDescendants(child, namespace, result)); } } /** * Returns structured metadata for a component or component set defined in the current file. * * @param {string} componentId - The node ID of a COMPONENT or COMPONENT_SET node. * @returns {Promise<{name: string, nodeId: string, properties: object, descendants: object}|undefined>} */ async function getLocalComponentMetadata(componentId) { const node = await figma.getNodeByIdAsync(componentId); if (node.type === "COMPONENT_SET" || node.type === "COMPONENT") { const result = { name: node.name, nodeId: node.id, properties: {}, descendants: {} }; result.properties = getComponentProps(node); collectDescendants(node, [], result.descendants); return result; } else { throw new Error("Node is not a Component or Component Set"); } } /** * Returns structured metadata for a published component or component set loaded by its key. * * @param {string} componentKey - The published key of the component or component set. * @returns {Promise<{name: string, nodeId: string, properties: object, descendants: object}>} */ async function getPublishedComponentMetadata(componentKey) { const node = await importComponentByKey(componentKey); const result = { name: node.name, nodeId: node.id, properties: {}, descendants: {} }; result.properties = getComponentProps(node); collectDescendants(node, [], result.descendants); return result; } ``` ### Full metadata extraction script ```javascript // For local components, use getLocalComponentMetadata: const result = await getLocalComponentMetadata('COMPONENT_OR_SET_ID'); return result; // For published components, use getPublishedComponentMetadata: // const result = await getPublishedComponentMetadata('COMPONENT_KEY'); // return result; ```