Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Official Figma skill for writing directly to the Figma canvas through the MCP server and Plugin API.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
component-patterns.md
1# Component & Variant API Patterns23> Part of the [use_figma skill](../SKILL.md). How to correctly use the Plugin API for components, variants, and component properties.4>5> 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).67## Contents89- Creating a Component10- Combining Components into a Component Set (Variants)11- Laying Out Variants After combineAsVariants (Required)12- Component Properties: addComponentProperty API13- Linking Properties to Child Nodes (Required)14- INSTANCE_SWAP: Avoiding Variant Explosion15- Discovering Existing Conventions in the File16- Importing Components by Key17- Working with Instances (finding variants, setProperties, text overrides, detachInstance)181920## Creating a Component2122`figma.createComponent()` returns a `ComponentNode`, which behaves like a `FrameNode` but can be published, instanced, and combined into variant sets.2324```javascript25const comp = figma.createComponent();26comp.name = "MyComponent";27comp.layoutMode = "HORIZONTAL";28comp.primaryAxisAlignItems = "CENTER";29comp.counterAxisAlignItems = "CENTER";30comp.paddingLeft = 12;31comp.paddingRight = 12;32comp.layoutSizingHorizontal = "HUG";33comp.layoutSizingVertical = "HUG";34comp.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 } }];35```3637## Combining Components into a Component Set (Variants)3839`figma.combineAsVariants(components, parent)` takes an array of `ComponentNode`s (not frames — frames will throw) and groups them into a `ComponentSetNode`.4041Variant 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.4243```javascript44// Each component's name encodes its variant properties45const comp1 = figma.createComponent();46comp1.name = "size=md, style=primary";47const comp2 = figma.createComponent();48comp2.name = "size=md, style=secondary";4950const componentSet = figma.combineAsVariants([comp1, comp2], figma.currentPage);51componentSet.name = "Button";52```5354**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.5556## Laying Out Variants After combineAsVariants (Required)5758After `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.5960```javascript61const cs = figma.combineAsVariants(components, figma.currentPage);6263// Simple row layout64cs.children.forEach((child, i) => {65child.x = i * 150;66child.y = 0;67});6869// CRITICAL: resize the component set from actual child bounds70let maxX = 0, maxY = 0;71for (const child of cs.children) {72maxX = Math.max(maxX, child.x + child.width);73maxY = Math.max(maxY, child.y + child.height);74}75cs.resizeWithoutConstraints(maxX + 40, maxY + 40);76```7778For multi-axis variants (e.g., size × style × state), parse the child's name to determine grid position:7980```javascript81for (const child of cs.children) {82const props = Object.fromEntries(83child.name.split(', ').map(p => p.split('='))84);85const col = stateValues.indexOf(props.state);86const row = styleValues.indexOf(props.style);87child.x = col * colWidth;88child.y = row * rowHeight;89}90```9192## Component Properties: addComponentProperty API9394`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.9596```javascript97// Returns the key as a string — capture it!98const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Default text');99const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true);100const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComponentId);101```102103**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.104105## Linking Properties to Child Nodes (Required)106107A property that is added but not linked to a child node does **nothing**. You must set `componentPropertyReferences` on the child:108109```javascript110// TEXT property → link to a text node's characters111const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button');112const textNode = figma.createText();113textNode.characters = "Button";114comp.appendChild(textNode);115textNode.componentPropertyReferences = { characters: labelKey };116117// BOOLEAN + INSTANCE_SWAP → link to an instance node118const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true);119const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id);120const iconInstance = iconComp.createInstance();121comp.appendChild(iconInstance);122iconInstance.componentPropertyReferences = {123visible: showIconKey, // BOOLEAN controls show/hide124mainComponent: iconSlotKey // INSTANCE_SWAP controls which component125};126```127128**Valid `componentPropertyReferences` keys:**129- `characters` — TEXT property on a TextNode130- `visible` — BOOLEAN property (any node)131- `mainComponent` — INSTANCE_SWAP property on an InstanceNode132133## INSTANCE_SWAP: Avoiding Variant Explosion134135When 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.136137```javascript138// Create icon as its own ComponentNode139const iconComp = figma.createComponent();140iconComp.name = "Icon/Search";141iconComp.resize(24, 24);142const svgNode = figma.createNodeFromSvg('<svg>...</svg>');143iconComp.appendChild(svgNode);144145// Use it as the default for INSTANCE_SWAP146const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id);147const instance = iconComp.createInstance();148comp.appendChild(instance);149instance.componentPropertyReferences = { mainComponent: iconSlotKey };150```151152This works for icons, avatars, badges, or any swappable nested element.153154## Discovering Existing Conventions in the File155156**Always inspect the file before creating components.** Different files have different naming styles, structures, and conventions. Your code should match what's already there.157158### List all existing components across all pages159160```javascript161const results = [];162for (const page of figma.root.children) {163await figma.setCurrentPageAsync(page);164page.findAll(n => {165if (n.type === 'COMPONENT') results.push(`[${page.name}] ${n.name} (COMPONENT) id=${n.id}`);166if (n.type === 'COMPONENT_SET') results.push(`[${page.name}] ${n.name} (COMPONENT_SET) id=${n.id}`);167return false;168});169}170return results.join('\n');171```172173### Inspect an existing component set's variant naming pattern174175```javascript176const cs = await figma.getNodeByIdAsync('COMPONENT_SET_ID');177const variantNames = cs.children.map(c => c.name);178const propDefs = cs.componentPropertyDefinitions;179return { variantNames, propDefs };180```181182### Find existing components in the file183184```javascript185const components = [];186for (const page of figma.root.children) {187await figma.setCurrentPageAsync(page);188page.findAll(n => {189if (n.type === 'COMPONENT') {190components.push({ name: n.name, id: n.id, page: page.name, w: n.width, h: n.height });191}192return false;193});194}195return components;196```197198## Importing Components by Key (Team Libraries)199200`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.201202```javascript203// Import a component from a team library204const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY");205const instance = comp.createInstance();206207// Import a component set from a team library and pick a variant208const set = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY");209const variant = set.children.find(c =>210c.type === "COMPONENT" && c.name.includes("size=md")211) || set.defaultVariant;212const variantInstance = variant.createInstance();213```214215## Working with Instances216217### Finding the right variant in a component set218219Parse variant names to match on multiple properties simultaneously:220221```javascript222const compSet = await figma.importComponentSetByKeyAsync("KEY");223224const variant = compSet.children.find(c => {225const props = Object.fromEntries(226c.name.split(', ').map(p => p.split('='))227);228return props.variant === "primary" && props.size === "md";229}) || compSet.defaultVariant;230231const instance = variant.createInstance();232```233234### Setting variant properties on an instance235236After creating an instance from a component set, you can set variant properties via `setProperties`:237238```javascript239const instance = defaultVariant.createInstance();240instance.setProperties({241"variant": "primary",242"size": "medium"243});244```245246### Overriding text in a component instance247248**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.249250**Step 1: Inspect componentProperties on a sample instance:**251252```javascript253const instance = comp.createInstance();254const propDefs = instance.componentProperties;255// Returns e.g.: { "Label#2:0": { type: "TEXT", value: "Button" }, "Has Icon#4:64": { type: "BOOLEAN", value: true } }256return propDefs;257```258259Also check nested instances — a parent component may not expose text properties directly, but its nested child instances might:260261```javascript262const nestedInstances = instance.findAll(n => n.type === "INSTANCE");263const nestedProps = nestedInstances.map(ni => ({264name: ni.name,265id: ni.id,266properties: ni.componentProperties267}));268```269270**Step 2: Use setProperties() for TEXT-type properties:**271272```javascript273const instance = comp.createInstance();274const propDefs = instance.componentProperties;275for (const [key, def] of Object.entries(propDefs)) {276if (def.type === "TEXT") {277instance.setProperties({ [key]: "New text value" });278}279}280```281282For nested instances that expose their own TEXT properties, call `setProperties()` on the nested instance:283284```javascript285const nestedHeading = instance.findOne(n => n.type === "INSTANCE" && n.name === "Text Heading");286if (nestedHeading) {287nestedHeading.setProperties({ "Text#2104:5": "Actual heading text" });288}289```290291**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:292293```javascript294const textNodes = instance.findAll(n => n.type === "TEXT");295for (const t of textNodes) {296await figma.loadFontAsync(t.fontName);297t.characters = "Updated text";298}299```300301### detachInstance() invalidates ancestor node IDs302303**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.304305```javascript306// WRONG — cached parent ID becomes invalid after child detach307const parentId = parentInstance.id;308nestedChild.detachInstance();309const parent = await figma.getNodeByIdAsync(parentId); // null!310311// CORRECT — re-discover nodes by traversal from a stable (non-instance) parent312const stableFrame = await figma.getNodeByIdAsync(manualFrameId); // a frame YOU created313nestedChild.detachInstance();314// Re-find the parent by traversing from the stable frame315const parent = stableFrame.findOne(n => n.name === "ParentName");316```317318If 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.319320## Inspecting Component Metadata (Deep Traversal)321322These helpers extract the full property schema and descendant structure of a component. Useful for understanding complex components before creating instances or setting properties.323324```javascript325/**326* Imports a component or component set from a library by its published key.327* Tries COMPONENT first, then falls back to COMPONENT_SET.328*329* @param {string} componentKey - The published key of the component or component set.330* @returns {Promise<ComponentNode|ComponentSetNode>}331*/332async function importComponentByKey(componentKey) {333try {334return await figma.importComponentByKeyAsync(componentKey);335} catch {336try {337return await figma.importComponentSetByKeyAsync(componentKey);338} catch {339throw new Error(`No Component or Component Set available with key '${componentKey}'`);340}341}342}343344/**345* Given a main component node, returns the component set parent if one exists,346* otherwise returns the component itself. Used to get the top-level node that347* holds `componentPropertyDefinitions`.348*349* @param {ComponentNode} mainComponent350* @returns {ComponentNode|ComponentSetNode}351*/352function getRelevantComponentNode(mainComponent) {353return mainComponent.parent.type === "COMPONENT_SET"354? mainComponent.parent355: mainComponent;356}357358/**359* Extracts `componentPropertyDefinitions` from a component or component set node360* into a flat map keyed by property key.361*362* @param {ComponentNode|ComponentSetNode} node363* @returns {Record<string, {name: string, type: string, key: string, variantOptions?: string[]}>}364*/365function getComponentProps(node) {366const result = {};367for (let key in node.componentPropertyDefinitions) {368const prop = {369name: key.replace(/#[^#]+$/, ""),370type: node.componentPropertyDefinitions[key].type,371key: key372};373if (prop.type === "VARIANT") {374prop.variantOptions = node.componentPropertyDefinitions[key].variantOptions;375}376result[key] = prop;377}378return result;379}380381/**382* Recursively walks a component tree and collects all INSTANCE and TEXT nodes383* into `result`, keyed by `TYPE[name]`. Handles variant namespacing and384* deduplicates nodes with identical names but differing property references.385*386* @param {SceneNode} node - The node to traverse.387* @param {string[]} namespace - Accumulated variant names for the current path.388* @param {Record<string, object>} result - Accumulator object populated in place.389*/390function collectDescendants(node, namespace, result) {391if (node.type === "INSTANCE" || node.type === "TEXT") {392const references = node.componentPropertyReferences || {};393if (!node.visible && !references.visible) return;394395const object = { type: node.type, name: node.name, references };396let key = `${node.type}[${node.name}]`;397398if (result[key] && JSON.stringify(references) !== JSON.stringify(result[key].references)) {399key += btoa(btoa(unescape(encodeURIComponent(JSON.stringify(references)))));400}401402if (node.type === "INSTANCE") {403const mainComponent = getRelevantComponentNode(node.mainComponent);404object.properties = getComponentProps(mainComponent);405object.descendants = {};406object.mainComponentName = mainComponent.name;407collectDescendants(mainComponent, [], object.descendants);408}409410const start = namespace.length ? { variants: [] } : {};411result[key] = Object.assign(object, result[key] || start);412if (namespace.length) result[key].variants.push(namespace[namespace.length - 1]);413} else if ("children" in node && node.visible) {414if (node.type === "COMPONENT" && node.parent.type === "COMPONENT_SET") namespace.push(node.name);415node.children.forEac