Loading...
Loading...
Figma workflows, components, auto layout, constraints, prototyping, design systems, and plugin development based on Figma Plugin API documentation
npx skill4agent add manutej/luxor-claude-marketplace figma-designDocumentNode (root)
└── PageNode
├── FrameNode
│ ├── TextNode
│ ├── RectangleNode
│ └── ComponentNode
└── SectionNode
└── FrameNodeFRAMECOMPONENTINSTANCETEXTRECTANGLEELLIPSEPOLYGONSTARLINESECTIONGROUP// Create a component
const button = figma.createComponent()
button.name = "Primary Button"
button.resize(120, 40)
// Add visual elements
const bg = figma.createRectangle()
bg.resize(120, 40)
bg.cornerRadius = 8
bg.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.5, b: 1 } }]
button.appendChild(bg)
// Create instance
const buttonInstance = button.createInstance()
buttonInstance.x = 200
buttonInstance.y = 100BOOLEANTEXTINSTANCE_SWAPVARIANTlayoutModeprimaryAxisSizingModecounterAxisSizingModepaddingLeftpaddingRightpaddingToppaddingBottomitemSpacingprimaryAxisAlignItemscounterAxisAlignItemsminWidthmaxWidthminHeightmaxHeightlayoutAlignlayoutGrow// Create auto-layout frame
const frame = figma.createFrame()
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO'
frame.counterAxisSizingMode = 'FIXED'
frame.resize(300, 0) // Width fixed, height auto
frame.itemSpacing = 16
frame.paddingLeft = 24
frame.paddingRight = 24
frame.paddingTop = 24
frame.paddingBottom = 24
// Add children with constraints
const child = figma.createRectangle()
child.resize(252, 100)
child.layoutAlign = 'STRETCH' // Fill width
child.minHeight = 100
child.maxHeight = 200
frame.appendChild(child)interface Constraints {
horizontal: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'SCALE'
vertical: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'SCALE'
}
node.constraints = {
horizontal: 'MIN', // Pin to left
vertical: 'MAX' // Pin to bottom
}MINCENTERMAXSTRETCHSCALE// Create variable collection
const collection = figma.variables.createVariableCollection('Design Tokens')
// Create color variable
const primaryColor = figma.variables.createVariable(
'color/primary',
collection,
'COLOR'
)
// Set value for default mode
const defaultMode = collection.modes[0]
primaryColor.setValueForMode(defaultMode.modeId, {
r: 0.2, g: 0.5, b: 1, a: 1
})
// Add dark mode
const darkMode = collection.addMode('Dark')
primaryColor.setValueForMode(darkMode, {
r: 0.4, g: 0.7, b: 1, a: 1
})
// Create variable alias (reference)
const accentColor = figma.variables.createVariable(
'color/accent',
collection,
'COLOR'
)
const alias = figma.variables.createVariableAlias(primaryColor)
accentColor.setValueForMode(defaultMode.modeId, alias)
// Bind variable to node
const rect = figma.createRectangle()
const fill = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } } as SolidPaint
const boundFill = figma.variables.setBoundVariableForPaint(
fill,
'color',
primaryColor
)
rect.fills = [boundFill]COLORFLOATBOOLEANSTRING// Paint style (fills/strokes)
const paintStyle = figma.createPaintStyle()
paintStyle.name = 'Brand/Primary'
paintStyle.paints = [{
type: 'SOLID',
color: { r: 0.2, g: 0.5, b: 1 }
}]
// Text style
const textStyle = figma.createTextStyle()
textStyle.name = 'Heading/H1'
textStyle.fontSize = 32
textStyle.fontName = { family: 'Inter', style: 'Bold' }
textStyle.lineHeight = { value: 120, unit: 'PERCENT' }
textStyle.letterSpacing = { value: -0.5, unit: 'PIXELS' }
// Effect style (shadows, blurs)
const effectStyle = figma.createEffectStyle()
effectStyle.name = 'Shadow/Card'
effectStyle.effects = [{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.15 },
offset: { x: 0, y: 4 },
radius: 8,
visible: true,
blendMode: 'NORMAL'
}]
// Apply styles
rect.fillStyleId = paintStyle.id
text.textStyleId = textStyle.id
frame.effectStyleId = effectStyle.id// Set reactions on a node
await node.setReactionsAsync([
{
action: {
type: 'NODE',
destinationId: 'nodeId', // Target frame
navigation: 'NAVIGATE',
transition: {
type: 'SMART_ANIMATE',
easing: { type: 'EASE_IN_AND_OUT' },
duration: 0.3
},
preserveScrollPosition: false
},
trigger: {
type: 'ON_CLICK'
}
}
])ON_CLICKON_HOVERON_PRESSON_DRAGMOUSE_ENTERMOUSE_LEAVEMOUSE_UPMOUSE_DOWNAFTER_TIMEOUTNAVIGATESWAPOVERLAYSCROLL_TOCHANGE_TOframe.overlayPositionType // 'CENTER' | 'TOP_LEFT' | 'TOP_CENTER' | etc.
frame.overlayBackground // How overlay obscures background
frame.overlayBackgroundInteraction // Click-through behaviorframe.overflowDirection = 'VERTICAL_SCROLLING' // or 'HORIZONTAL_SCROLLING'
frame.numberOfFixedChildren = 1 // First N children stay fixed during scrollmy-plugin/
├── manifest.json # Plugin configuration
├── code.ts # Main plugin logic
└── ui.html # Optional UI{
"name": "My Plugin",
"id": "unique-plugin-id",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": ["figma", "figjam"],
"documentAccess": "dynamic-page",
"networkAccess": {
"allowedDomains": ["api.example.com"]
}
}// Initialize plugin
async function init() {
// Show UI (optional)
figma.showUI(__html__, {
width: 400,
height: 500,
title: "My Plugin",
themeColors: true
})
// Load saved preferences
const prefs = await figma.clientStorage.getAsync('preferences')
// Send initial data to UI
figma.ui.postMessage({
type: 'init',
data: prefs
})
}
// Handle UI messages
figma.ui.onmessage = async (msg) => {
if (msg.type === 'create-shapes') {
await createShapes(msg.count, msg.color)
}
if (msg.type === 'export') {
await exportSelection()
}
}
// Clean up on close
figma.on('close', () => {
console.log('Plugin closing...')
})
// Start plugin
init()figma.ui.postMessage({
type: 'selection-changed',
count: figma.currentPage.selection.length,
nodes: figma.currentPage.selection.map(n => ({
id: n.id,
name: n.name,
type: n.type
}))
})<script>
window.onmessage = (event) => {
const msg = event.data.pluginMessage
if (msg.type === 'selection-changed') {
document.getElementById('count').textContent = msg.count
}
}
// Send message to plugin
function createShapes() {
parent.postMessage({
pluginMessage: {
type: 'create-shapes',
count: 5,
color: { r: 1, g: 0, b: 0 }
}
}, '*')
}
</script>// Selection changes
figma.on('selectionchange', () => {
const selection = figma.currentPage.selection
console.log(`Selected ${selection.length} nodes`)
})
// Document changes
figma.on('documentchange', (event) => {
for (const change of event.documentChanges) {
if (change.type === 'CREATE') {
console.log(`Created: ${change.id}`)
}
if (change.type === 'DELETE') {
console.log(`Deleted: ${change.id}`)
}
if (change.type === 'PROPERTY_CHANGE') {
console.log(`Changed properties: ${change.properties.join(', ')}`)
}
}
})
// Page changes
figma.on('currentpagechange', () => {
console.log(`Now on page: ${figma.currentPage.name}`)
})// Store data on node
node.setPluginData('status', 'approved')
node.setPluginData('metadata', JSON.stringify({
author: 'John',
tags: ['important']
}))
// Retrieve data
const status = node.getPluginData('status')
const metadata = JSON.parse(node.getPluginData('metadata') || '{}')
// List all keys
const keys = node.getPluginDataKeys()// Store shared data
node.setSharedPluginData('com.example.plugin', 'version', '2.0')
// Retrieve shared data
const version = node.getSharedPluginData('com.example.plugin', 'version')
// List shared keys
const keys = node.getSharedPluginDataKeys('com.example.plugin')// Save preferences
await figma.clientStorage.setAsync('preferences', {
theme: 'dark',
lastUsed: new Date().toISOString()
})
// Load preferences
const prefs = await figma.clientStorage.getAsync('preferences')
// Delete data
await figma.clientStorage.deleteAsync('preferences')
// List all keys
const keys = await figma.clientStorage.keysAsync()// Enable performance optimization
figma.skipInvisibleInstanceChildren = true
// Find by type
const textNodes = figma.currentPage.findAllWithCriteria({
types: ['TEXT']
})
// Find multiple types
const shapes = figma.currentPage.findAllWithCriteria({
types: ['RECTANGLE', 'ELLIPSE', 'POLYGON']
})
// Find nodes with plugin data
const nodesWithStatus = figma.currentPage.findAllWithCriteria({
pluginData: {
keys: ['status']
}
})
// Find nodes with shared plugin data
const nodesWithSharedData = figma.currentPage.findAllWithCriteria({
sharedPluginData: {
namespace: 'com.example.plugin',
keys: ['version']
}
})
// Combine criteria
const textWithData = figma.currentPage.findAllWithCriteria({
types: ['TEXT'],
pluginData: {} // Any plugin data
})// Find all matching nodes
const frames = figma.currentPage.findAll(node => node.type === 'FRAME')
// Find first match
const template = figma.currentPage.findOne(node =>
node.name.startsWith('Template')
)
// Find by ID
const node = await figma.getNodeByIdAsync('123:456')
// Recursive tree traversal
function walkTree(node: BaseNode) {
console.log(`${node.type}: ${node.name}`)
if ('children' in node) {
for (const child of node.children) {
walkTree(child)
}
}
}
walkTree(figma.currentPage)// Export as PNG
const pngBytes = await node.exportAsync({
format: 'PNG',
constraint: { type: 'SCALE', value: 2 } // 2x resolution
})
// Export as JPG
const jpgBytes = await node.exportAsync({
format: 'JPG',
constraint: { type: 'SCALE', value: 1 },
contentsOnly: false // Include background
})
// Export as SVG
const svgBytes = await node.exportAsync({
format: 'SVG',
svgIdAttribute: true,
svgOutlineText: false,
svgSimplifyStroke: true
})
// Export with fixed dimensions
const thumbnail = await node.exportAsync({
format: 'PNG',
constraint: { type: 'WIDTH', value: 200 }
})
// Export with height constraint
const preview = await node.exportAsync({
format: 'PNG',
constraint: { type: 'HEIGHT', value: 400 }
})// Load from URL
const image = await figma.createImageAsync('https://example.com/image.png')
const { width, height } = await image.getSizeAsync()
// Create rectangle with image
const rect = figma.createRectangle()
rect.resize(width, height)
rect.fills = [{
type: 'IMAGE',
imageHash: image.hash,
scaleMode: 'FILL' // or 'FIT', 'CROP', 'TILE'
}]
// Load from bytes
function loadFromBytes(bytes: Uint8Array) {
const image = figma.createImage(bytes)
return image
}
// Create from SVG string
const svgString = `
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" fill="#3366ff"/>
</svg>
`
const node = figma.createNodeFromSvg(svgString)// Create text (MUST load font first)
const text = figma.createText()
await figma.loadFontAsync(text.fontName) // Load default font
// Set content
text.characters = 'Hello World'
// Change font
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' })
text.fontName = { family: 'Inter', style: 'Bold' }
// Styling
text.fontSize = 24
text.lineHeight = { value: 150, unit: 'PERCENT' }
text.letterSpacing = { value: 0, unit: 'PIXELS' }
text.textAlignHorizontal = 'CENTER'
text.textAlignVertical = 'CENTER'
text.textCase = 'UPPER' // or 'LOWER', 'TITLE', 'ORIGINAL'
text.textDecoration = 'UNDERLINE' // or 'STRIKETHROUGH', 'NONE'// Apply formatting to range
text.setRangeFontSize(0, 5, 32) // First 5 chars = 32px
text.setRangeFills(0, 5, [{
type: 'SOLID',
color: { r: 1, g: 0, b: 0 }
}])
// Get font at position
const fontName = text.getRangeFontName(0, 1)
const fontSize = text.getRangeFontSize(0, 1)// Resize with constraints applied
node.resize(300, 200)
// Resize without constraints
node.resizeWithoutConstraints(300, 200)
// Scale proportionally
node.rescale(1.5) // 150% scale// Position
node.x = 100
node.y = 200
// Relative to parent
console.log(node.relativeTransform) // [[1,0,x], [0,1,y]]
// Absolute position on page
console.log(node.absoluteTransform)
// Bounding box
const bounds = node.absoluteBoundingBox
// { x: number, y: number, width: number, height: number }// Plugin data can track custom versioning
node.setPluginData('version', '2.1.0')
node.setPluginData('lastModified', new Date().toISOString())
node.setPluginData('modifiedBy', 'user@example.com')// Check if node is from external library
if (node.type === 'INSTANCE') {
const component = node.mainComponent
if (component && component.parent?.type === 'PAGE') {
// Component is from library
}
}// ✅ Use findAllWithCriteria for large documents
const nodes = figma.currentPage.findAllWithCriteria({
types: ['TEXT']
})
// ❌ Avoid findAll for simple type searches
const nodes = figma.currentPage.findAll(n => n.type === 'TEXT')
// ✅ Enable invisible instance optimization
figma.skipInvisibleInstanceChildren = true
// ✅ Batch operations
const nodes = []
for (let i = 0; i < 100; i++) {
const rect = figma.createRectangle()
rect.x = i * 120
nodes.push(rect)
}
figma.currentPage.selection = nodes
figma.viewport.scrollAndZoomIntoView(nodes)
// ❌ Avoid individual viewport updates
for (let i = 0; i < 100; i++) {
const rect = figma.createRectangle()
figma.viewport.scrollAndZoomIntoView([rect]) // Slow!
}// ✅ Always load fonts before modifying text
const text = figma.createText()
await figma.loadFontAsync(text.fontName)
text.characters = 'Hello'
// ✅ Load font before changing fontName
await figma.loadFontAsync({ family: 'Roboto', style: 'Bold' })
text.fontName = { family: 'Roboto', style: 'Bold' }
// ❌ This will error
text.characters = 'Hello' // Error: font not loadedtry {
const node = await figma.getNodeByIdAsync(id)
if (!node) {
figma.notify('Node not found', { error: true })
return
}
// Process node
} catch (error) {
figma.notify(`Error: ${error.message}`, { error: true })
console.error(error)
}// ✅ Clean up large data
figma.on('close', () => {
// Clear large caches
cache.clear()
})
// ✅ Use async iteration for large datasets
async function processManyNodes() {
const nodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] })
for (let i = 0; i < nodes.length; i++) {
await processNode(nodes[i])
// Yield to UI every 100 items
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0))
}
}
}// ✅ Type guards
if (node.type === 'TEXT') {
// TypeScript knows node is TextNode
console.log(node.characters)
}
if ('children' in node) {
// Node has children
node.children.forEach(child => console.log(child.name))
}
// ✅ Use specific types
function processText(node: TextNode) {
console.log(node.characters)
}
// ❌ Avoid any
function processNode(node: any) { // Bad!
console.log(node.characters) // Might error
}async function setupDesignSystem() {
// Create variable collection
const tokens = figma.variables.createVariableCollection('Design Tokens')
// Colors
const colors = {
'color/primary': { r: 0.2, g: 0.5, b: 1, a: 1 },
'color/secondary': { r: 0.5, g: 0.2, b: 0.8, a: 1 },
'color/success': { r: 0.2, g: 0.8, b: 0.3, a: 1 },
'color/error': { r: 0.9, g: 0.2, b: 0.2, a: 1 }
}
for (const [name, value] of Object.entries(colors)) {
const variable = figma.variables.createVariable(name, tokens, 'COLOR')
variable.setValueForMode(tokens.modes[0].modeId, value)
}
// Spacing
const spacing = [4, 8, 12, 16, 24, 32, 48, 64]
spacing.forEach((value, i) => {
const variable = figma.variables.createVariable(
`spacing/${i}`,
tokens,
'FLOAT'
)
variable.setValueForMode(tokens.modes[0].modeId, value)
})
// Create styles
const primaryStyle = figma.createPaintStyle()
primaryStyle.name = 'Color/Primary'
primaryStyle.paints = [{
type: 'SOLID',
color: colors['color/primary']
}]
figma.notify('Design system created!')
}async function createButtonLibrary() {
// Create component set for variants
const buttons = figma.createComponentSet()
buttons.name = "Button"
// Primary variant
const primary = figma.createComponent()
primary.name = "Type=Primary, Size=Medium"
primary.resize(120, 40)
buttons.appendChild(primary)
// Secondary variant
const secondary = figma.createComponent()
secondary.name = "Type=Secondary, Size=Medium"
secondary.resize(120, 40)
buttons.appendChild(secondary)
// Large size
const large = figma.createComponent()
large.name = "Type=Primary, Size=Large"
large.resize(160, 48)
buttons.appendChild(large)
return buttons
}async function exportAllFrames() {
const frames = figma.currentPage.findAllWithCriteria({
types: ['FRAME']
})
const exports = []
for (const frame of frames) {
// Export 1x and 2x
const bytes1x = await frame.exportAsync({
format: 'PNG',
constraint: { type: 'SCALE', value: 1 }
})
const bytes2x = await frame.exportAsync({
format: 'PNG',
constraint: { type: 'SCALE', value: 2 }
})
exports.push({
name: frame.name,
'1x': bytes1x,
'2x': bytes2x
})
}
// Send to UI for download
figma.ui.postMessage({
type: 'exports-ready',
exports
})
}async function syncStyles() {
const paintStyles = await figma.getLocalPaintStylesAsync()
const textStyles = await figma.getLocalTextStylesAsync()
// Update all instances
const nodes = figma.currentPage.findAllWithCriteria({
types: ['RECTANGLE', 'TEXT']
})
for (const node of nodes) {
if (node.type === 'RECTANGLE') {
// Apply paint style based on name
const style = paintStyles.find(s => s.name === 'Brand/Primary')
if (style) {
node.fillStyleId = style.id
}
}
if (node.type === 'TEXT') {
// Apply text style
const style = textStyles.find(s => s.name === 'Body/Regular')
if (style) {
node.textStyleId = style.id
}
}
}
figma.notify('Styles synced!')
}<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 16px;
font-family: 'Inter', sans-serif;
background: var(--figma-color-bg);
color: var(--figma-color-text);
}
button {
background: var(--figma-color-bg-brand);
color: var(--figma-color-text-onbrand);
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: var(--figma-color-bg-brand-hover);
}
input {
background: var(--figma-color-bg);
color: var(--figma-color-text);
border: 1px solid var(--figma-color-border);
padding: 8px;
border-radius: 4px;
}
</style>
</head>
<body>
<h2>My Plugin</h2>
<input type="text" id="input" placeholder="Enter text...">
<button onclick="handleClick()">Create</button>
</body>
</html><div id="status">
<div class="spinner"></div>
<p>Processing...</p>
</div>
<script>
function showLoading() {
document.getElementById('status').style.display = 'block'
}
function hideLoading() {
document.getElementById('status').style.display = 'none'
}
async function process() {
showLoading()
parent.postMessage({ pluginMessage: { type: 'process' } }, '*')
}
window.onmessage = (event) => {
if (event.data.pluginMessage.type === 'complete') {
hideLoading()
}
}
</script>if (figma.editorType === 'figjam') {
// Create sticky note
const sticky = figma.createSticky()
sticky.x = 100
sticky.y = 100
await figma.loadFontAsync({ family: "Roboto", style: "Regular" })
sticky.text.characters = "TODO: Review designs"
// Create connector
const connector = figma.createConnector()
connector.connectorStart = {
endpointNodeId: sticky.id,
position: { x: 0, y: 0.5 }
}
// Create shape
const shape = figma.createShape()
shape.resize(100, 100)
shape.fills = [{ type: 'SOLID', color: { r: 1, g: 0.8, b: 0 } }]
}async function swapComponents(oldComponent: ComponentNode, newComponent: ComponentNode) {
const instances = figma.currentPage.findAllWithCriteria({
types: ['INSTANCE']
})
let swapCount = 0
for (const instance of instances) {
if (instance.mainComponent?.id === oldComponent.id) {
await instance.swapAsync(newComponent)
swapCount++
}
}
figma.notify(`Swapped ${swapCount} instances`)
}function convertToAutoLayout(frame: FrameNode) {
// Store original positions
const childData = frame.children.map(child => ({
node: child,
x: child.x,
y: child.y
}))
// Enable auto layout
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO'
frame.counterAxisSizingMode = 'FIXED'
frame.itemSpacing = 16
frame.paddingLeft = 24
frame.paddingRight = 24
frame.paddingTop = 24
frame.paddingBottom = 24
// Configure children
frame.children.forEach(child => {
if ('layoutAlign' in child) {
child.layoutAlign = 'STRETCH'
}
})
}function makeResponsive(frame: FrameNode) {
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO'
frame.counterAxisSizingMode = 'FIXED'
// Make children responsive
frame.children.forEach(child => {
if ('resize' in child) {
child.layoutAlign = 'STRETCH'
child.layoutGrow = 0
child.minWidth = 200
child.maxWidth = 800
}
})
}// Console logging
console.log('Selection:', figma.currentPage.selection)
console.error('Error occurred:', error)
// Notifications
figma.notify('Success!', { timeout: 2000 })
figma.notify('Error occurred', { error: true })
// Debugging helpers
function debugNode(node: SceneNode) {
console.log('Node Debug Info:')
console.log(' Type:', node.type)
console.log(' Name:', node.name)
console.log(' ID:', node.id)
console.log(' Position:', { x: node.x, y: node.y })
if ('width' in node) {
console.log(' Size:', { width: node.width, height: node.height })
}
if ('children' in node) {
console.log(' Children:', node.children.length)
}
}