Last active
April 17, 2025 02:30
-
-
Save connorwforsyth/e76b05c47cb95d1a2774a75bbe9d06b5 to your computer and use it in GitHub Desktop.
Figma Icon Variant Builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Get current selection | |
| const selection = figma.currentPage.selection; | |
| // First, convert all selections to components if they aren't already | |
| const componentNodes = []; | |
| for (let i = 0; i < selection.length; i++) { | |
| const node = selection[i]; | |
| let component; | |
| // If node is already a component, use it; otherwise create a new component | |
| if (node.type === 'COMPONENT') { | |
| component = node; | |
| } else { | |
| // Create a new component | |
| component = figma.createComponent(); | |
| component.resize(node.width, node.height); | |
| component.x = node.x; | |
| component.y = node.y; | |
| // Handle different node types | |
| if (node.type === 'IMAGE') { | |
| // For images, we need to copy the image hash | |
| if (node.fills && node.fills.length > 0) { | |
| component.fills = [...node.fills]; | |
| } | |
| } else if (node.children && node.children.length > 0) { | |
| // For nodes with children, clone each child | |
| for (const child of node.children) { | |
| const clone = child.clone(); | |
| component.appendChild(clone); | |
| } | |
| } else { | |
| // For other node types, try to copy visual properties | |
| if (node.fills) component.fills = [...node.fills]; | |
| if (node.strokes) component.strokes = [...node.strokes]; | |
| if (node.effects) component.effects = [...node.effects]; | |
| if (node.cornerRadius !== undefined) component.cornerRadius = node.cornerRadius; | |
| } | |
| // Set component name to match the original node | |
| component.name = node.name; | |
| // Replace the original node with the component | |
| if (node.parent) { | |
| node.parent.insertChild(node.parent.children.indexOf(node), component); | |
| node.remove(); | |
| } | |
| } | |
| componentNodes.push(component); | |
| } | |
| // Group components by their icon name | |
| const groupedComponents = {}; | |
| const standaloneComponents = []; | |
| for (const component of componentNodes) { | |
| const nameParts = component.name.split('_'); | |
| if (nameParts.length < 2) { | |
| standaloneComponents.push(component); | |
| continue; // Skip if not enough parts | |
| } | |
| const number = nameParts[0]; // e.g., "001" | |
| // Get the name part (everything between number and variant type) | |
| const namePartArray = nameParts.slice(1, nameParts.length - 1); | |
| if (namePartArray.length === 0) { | |
| standaloneComponents.push(component); | |
| continue; // Skip if no middle parts | |
| } | |
| const namePart = namePartArray.join('_'); | |
| const groupKey = `${number}_${namePart}`; | |
| if (!groupedComponents[groupKey]) { | |
| groupedComponents[groupKey] = []; | |
| } | |
| groupedComponents[groupKey].push(component); | |
| } | |
| // Create a component set for each icon name | |
| const componentSets = []; | |
| for (const groupKey in groupedComponents) { | |
| const components = groupedComponents[groupKey]; | |
| // Skip if there's only one component (need at least 2 for a component set) | |
| if (components.length < 2) { | |
| standaloneComponents.push(...components); | |
| continue; | |
| } | |
| // Set variant properties for each component in this group | |
| for (const comp of components) { | |
| const nameParts = comp.name.split('_'); | |
| const variantType = nameParts[nameParts.length - 1]; // circle, grey01, outlined, white | |
| // Set only the Type variant property | |
| comp.name = `Type=${variantType}`; | |
| } | |
| // Combine components for this icon name into a component set | |
| const componentSet = figma.combineAsVariants(components, figma.currentPage); | |
| // Apply auto layout only to component sets | |
| componentSet.layoutMode = "HORIZONTAL"; | |
| componentSet.primaryAxisAlignItems = "CENTER"; | |
| componentSet.counterAxisAlignItems = "CENTER"; | |
| componentSet.paddingLeft = 0; | |
| componentSet.paddingRight = 0; | |
| componentSet.paddingTop = 0; | |
| componentSet.paddingBottom = 0; | |
| componentSet.itemSpacing = 0; | |
| // Set height to "Hug Contents" | |
| componentSet.counterAxisSizingMode = "AUTO"; | |
| // Extract a nice name for the component set without the number | |
| const nameParts = groupKey.split('_'); | |
| // Skip the first element (the number) | |
| const namePartArray = nameParts.slice(1); | |
| const displayName = namePartArray.map(word => | |
| word.charAt(0).toUpperCase() + word.slice(1) | |
| ).join(' '); | |
| // Set the component set name without the number | |
| componentSet.name = displayName; | |
| componentSets.push(componentSet); | |
| } | |
| // Process standalone components - just clean up their names | |
| for (const component of standaloneComponents) { | |
| const name = component.name; | |
| // Extract the number prefix (first part before underscore) | |
| const numberMatch = name.match(/^(\d+)_/); | |
| if (numberMatch) { | |
| // Get the base name without the number | |
| let baseName = name.substring(numberMatch[0].length); | |
| // Format the name nicely | |
| const displayName = baseName.split('_') | |
| .map(word => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join(' '); | |
| // Set the component name without the number | |
| component.name = displayName; | |
| } | |
| } | |
| console.log(`Created ${componentSets.length} component sets with auto layout and height set to "Hug Contents"`); | |
| console.log(`Kept ${standaloneComponents.length} standalone components with cleaned-up names`); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Select the icons you want to turn into a component set. Depending on their naming convention - this should create all the variants you need.