Working with text (fonts, mixed styles, missing fonts), images (add, manipulate, CORS), and vector paths (SVG data) in Figma plugins.
const text = figma.createText();
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
text.characters = 'Hello World';
text.fontSize = 24;
text.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }];
You MUST call loadFontAsync before modifying text content or layout properties.
// Single font
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
// All fonts on a node
await Promise.all(
text.getRangeAllFontNames(0, text.characters.length)
.map(figma.loadFontAsync)
);
characters, , , fontSizefontNametextStyleIdtextCase, textDecoration, letterSpacing, leadingTrim, lineHeightsetRange*() methods for the above.fills, .fillStyleId, .strokes, .strokeWeight, .strokeAlign, .dashPatternText can have different styles per character range:
// Check if property is mixed
const size = text.fontSize; // number | typeof figma.mixed
// Get style for specific range
text.getRangeFontName(0, 5); // FontName for chars 0-5
// Set style for specific range
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
text.setRangeFontName(0, 5, { family: 'Inter', style: 'Bold' });
text.setRangeFontSize(6, 11, 32);
// Get all styled segments
text.getStyledTextSegments(['fontName', 'fontSize']);
// Returns: [{ characters, start, end, fontName, fontSize }, ...]
if (text.hasMissingFont) {
figma.notify('Cannot edit — missing font');
return;
}
loadFontAsync will fail for missing fonts.hasMissingFont before editing existing text nodes.const image = await figma.createImageAsync('https://example.com/photo.jpg');
const { width, height } = await image.getSizeAsync();
const rect = figma.createRectangle();
rect.resize(width, height);
rect.fills = [{
type: 'IMAGE',
imageHash: image.hash,
scaleMode: 'FILL' // 'FILL' | 'FIT' | 'CROP' | 'TILE'
}];
// From Uint8Array (e.g., received from UI iframe)
const image = figma.createImage(new Uint8Array(bytes));
rect.fills = [{ type: 'IMAGE', imageHash: image.hash, scaleMode: 'FILL' }];
for (const paint of node.fills) {
if (paint.type === 'IMAGE') {
const image = figma.getImageByHash(paint.imageHash);
const bytes = await image.getBytesAsync();
// bytes is the encoded image file (PNG/JPG)
}
}
Images must be decoded in the UI iframe (which has <canvas>):
Step 1 — Sandbox sends bytes to UI:
const image = figma.getImageByHash(paint.imageHash);
const bytes = await image.getBytesAsync();
figma.showUI(__html__, { visible: false });
figma.ui.postMessage(bytes);
Step 2 — UI decodes, modifies, re-encodes:
<script>
window.onmessage = async (event) => {
const bytes = event.data.pluginMessage;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Decode
const blob = new Blob([bytes]);
const url = URL.createObjectURL(blob);
const img = await new Promise((resolve) => {
const i = new Image();
i.onload = () => resolve(i);
i.src = url;
});
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// Manipulate pixels
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// ... modify imageData.data ...
ctx.putImageData(imageData, 0, 0);
// Re-encode
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () => {
const newBytes = new Uint8Array(reader.result);
parent.postMessage({ pluginMessage: newBytes }, '*');
};
reader.readAsArrayBuffer(blob);
});
};
</script>
Step 3 — Sandbox creates new image:
figma.ui.onmessage = (newBytes) => {
const newImage = figma.createImage(newBytes);
node.fills = [{ ...paint, imageHash: newImage.hash }];
};
const vector = figma.createVector();
vector.vectorPaths = [{
windingRule: 'NONZERO', // 'NONZERO' | 'EVENODD'
data: 'M 0 0 L 100 0 L 100 100 L 0 100 Z'
}];
vector.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }];
vector.resize(100, 100);
Uses standard SVG path commands:
M x y — Move toL x y — Line toC x1 y1 x2 y2 x y — Cubic bezierQ x1 y1 x y — Quadratic bezierA rx ry rotation large-arc sweep x y — ArcZ — Close pathvector.vectorPaths = [
{ windingRule: 'NONZERO', data: 'M 0 0 L 50 0 L 25 50 Z' },
{ windingRule: 'NONZERO', data: 'M 60 0 L 110 0 L 85 50 Z' }
];
if (node.type === 'VECTOR') {
for (const path of node.vectorPaths) {
console.log(path.data); // SVG path string
console.log(path.windingRule); // 'NONZERO' | 'EVENODD'
}
}
const circle = figma.createEllipse();
circle.resize(100, 100);
const rect = figma.createRectangle();
rect.resize(60, 60);
rect.x = 20;
rect.y = 20;
// Subtract rect from circle
const result = figma.subtract([circle, rect], figma.currentPage);