Last month I shipped Kigen, this massive Figma plugin that grew to 400K lines of code. Building it was a journey, especially when it came to creating documentation generators programmatically.
When you're building documentation generators, you can't just drag and drop stuff. You have to create every single frame, text, and shape with code. And Figma's way of doing this? It's quite verbose.
Picture this: I want to make a simple card. Here's what I had to write:
const frame = figma.createFrame();
frame.name = "Card";
frame.layoutMode = "VERTICAL";
frame.primaryAxisSizingMode = "AUTO";
frame.counterAxisSizingMode = "AUTO";
frame.itemSpacing = 12;
frame.horizontalPadding = 16;
frame.verticalPadding = 16;
frame.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }];
frame.cornerRadius = 8;
// ... and more stuff for all customization
I was writing this pattern hundreds of times. It was repetitive work, and every time I wanted to change something small, I had to hunt through the entire codebase.
The Solution
Three months into building Kigen, I was looking at yet another 50-line function just to create a label next to some text. That's when I decided to build something better.
I didn't start by trying to make Figma's API slightly nicer. I started by writing what I WISHED I could write:
const card = FigmaLayoutManager.createLayoutFrame({
name: "Card",
direction: "VERTICAL",
padding: 16,
itemSpacing: 12,
fills: [{ type: "SOLID", color: COLORS.WHITE }],
cornerRadius: 8
});
That's it. Clean, simple, and no more typing primaryAxisSizingMode
ever again.
Building It Step by Step
Let me show you how I built this thing, because it didn't magically appear overnight.
Step 1: Just the Basics
Started with the absolute minimum:
static createLayoutFrame(options: { name: string; direction: "VERTICAL" | "HORIZONTAL" }): FrameNode {
const frame = figma.createFrame();
frame.name = options.name;
frame.layoutMode = options.direction;
return frame;
}
Even this tiny wrapper saved me from typing those three lines over and over.
Step 2: Adding the Stuff I Always Needed
After a week, I realized I was always setting the same boring properties:
static createLayoutFrame(options: {
name: string;
direction: "VERTICAL" | "HORIZONTAL";
padding?: number;
itemSpacing?: number;
}): FrameNode {
const frame = figma.createFrame();
frame.name = options.name;
frame.layoutMode = options.direction;
frame.primaryAxisSizingMode = "AUTO"; // Always needed this
frame.counterAxisSizingMode = "AUTO"; // And this
if (options.padding) {
frame.horizontalPadding = options.padding;
frame.verticalPadding = options.padding;
}
if (options.itemSpacing) {
frame.itemSpacing = options.itemSpacing;
}
return frame;
}
Better, but I kept needing more options.
Step 3: The Interface Got Crazy
As I used it more, I needed tons of options:
interface FrameOptions {
name: string;
direction: "VERTICAL" | "HORIZONTAL";
padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
itemSpacing?: number;
fills?: readonly Paint[] | null;
cornerRadius?: number;
stroke?: {
color?: RGB;
weight?: number;
align?: "INSIDE" | "OUTSIDE" | "CENTER";
};
// ... it kept growing and growing
}
This interface became huge, but it was useful huge.
Step 4: Breaking It Apart
The main function was getting too big, so I split it up:
static createLayoutFrame(options: FrameOptions): FrameNode {
const frame = figma.createFrame();
// Basic setup
frame.name = options.name;
frame.layoutMode = options.direction;
frame.primaryAxisSizingMode = "AUTO";
frame.counterAxisSizingMode = "AUTO";
// Let helper functions handle the details
this.setLayoutProperties(frame, options);
this.setStyling(frame, options);
this.setEnhancedProperties(frame, options);
return frame;
}
Much cleaner.
The Wrap Logic From Hell
The worst part was figuring out layout wrapping. Figma has this weird rule where wrapping only works on horizontal layouts. Took me 3 days to get this right:
// First try - crashed the plugin
if (options.wrap) {
frame.layoutWrap = "WRAP"; // Nope! Breaks on vertical layouts
}
// Second try - worked but confusing
if (options.wrap && options.direction === "HORIZONTAL") {
frame.layoutWrap = "WRAP";
}
// Final version - with helpful warnings
if (options.wrap && options.direction === "HORIZONTAL") {
frame.layoutWrap = "WRAP";
if (options.counterAxisSpacing !== undefined) {
frame.counterAxisSpacing = options.counterAxisSpacing;
}
} else {
frame.layoutWrap = "NO_WRAP";
// Save future me from confusion
if (options.wrap && options.direction === "VERTICAL") {
console.warn(`⚠️ Hey, you can't wrap vertical layouts!`);
}
}
Once basic frames worked, I started building components for stuff I did all the time:
For Example Label-Value Rows
Started simple:
// Version 1: Basic and boring
static createLabelValueRow(label: string, value: string) {
const labelText = figma.createText();
labelText.characters = label;
const valueText = figma.createText();
valueText.characters = value;
// ... lots more setup
}
Evolved into this:
static createLabelValueRow(
label: string,
value: string,
labelWidth: number = 90,
fontSize: number = 10
): FrameNode {
const row = this.createLayoutFrame({
name: label,
direction: "HORIZONTAL",
primaryAxis: "SPACE_BETWEEN"
});
const labelNode = this.createText({
content: label,
fontSize,
style: "Medium",
width: labelWidth
});
const valueNode = this.createText({
content: value,
fontSize,
style: "Semi Bold"
});
this.addChild(row, labelNode);
this.addChild(row, valueNode);
return row;
}
Token Frames for Design Systems
This one was crucial for Kigen's token docs:
static createTokenFrame(config: {
value: string;
alias?: string;
useAlias?: boolean;
useFill?: boolean;
radius?: number;
paddingHorizontal?: number;
paddingVertical?: number;
}): FrameNode {
// Handles all the complex token display logic
// Different styles for aliases vs values
// Color coding, spacing, everything
}
Font Loading Nightmare
Every text needs its font loaded first. This became its own utility:
export async function loadFontFromOptions(options: TextOptions): Promise<void> {
const fontFamily = options.fontFamily ?? "Inter";
const fontStyle = options.style ?? "Regular";
try {
// Try to find the exact font they asked for
const availableFonts = await figma.listAvailableFontsAsync();
const matchedFont = availableFonts
.map(f => f.fontName)
.find(f => f.family === fontFamily && f.style.toLowerCase() === fontStyle.toLowerCase());
const fontToLoad = matchedFont ?? { family: "Inter", style: "Regular" };
await figma.loadFontAsync(fontToLoad);
} catch (error) {
// When in doubt, use Inter and move on
console.warn(`⚠️ Font failed, using Inter instead`);
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
}
}
Real Examples from Building Kigen
Here's how this actually saved my life when building documentation features:
Token Table Headers
With my utility (4 lines):
const header = FigmaLayoutManager.createTokenTableHeader([
{ label: "Token", width: 250 },
{ label: "Value", width: 250 },
{ label: "Usage", width: 250 }
]);
Without it (the old nightmare - 58 lines):
const header = figma.createFrame();
header.name = "TokenTableHeader";
header.layoutMode = "HORIZONTAL";
header.primaryAxisSizingMode = "AUTO";
header.counterAxisSizingMode = "AUTO";
header.itemSpacing = 0;
header.fills = [{ type: "SOLID", color: { r: 0.96, g: 0.96, b: 0.96 } }];
header.strokes = [{ type: "SOLID", color: { r: 0.9, g: 0.9, b: 0.9 } }];
header.strokeWeight = 1;
header.strokeAlign = "CENTER";
// First column
const tokenText = figma.createText();
await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" });
tokenText.characters = "Token";
tokenText.fontSize = 16;
tokenText.fontName = { family: "Inter", style: "Semi Bold" };
tokenText.fills = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 } }];
const tokenWrapper = figma.createFrame();
tokenWrapper.name = "token-header-cell";
tokenWrapper.layoutMode = "VERTICAL";
tokenWrapper.primaryAxisSizingMode = "AUTO";
tokenWrapper.counterAxisSizingMode = "AUTO";
tokenWrapper.resize(250, tokenWrapper.height);
tokenWrapper.paddingTop = 12;
tokenWrapper.paddingBottom = 12;
tokenWrapper.paddingLeft = 20;
tokenWrapper.paddingRight = 20;
tokenWrapper.strokes = [{ type: "SOLID", color: { r: 0.9, g: 0.9, b: 0.9 } }];
tokenWrapper.strokeWeight = 1;
tokenWrapper.strokeAlign = "CENTER";
tokenWrapper.appendChild(tokenText);
// ... and this goes on for 40 more lines just for three columns!
Seriously, More than 50+ lines vs 4 lines. I had to write patterns like this dozens of times.
Component Cards
With my utility:
const card = FigmaLayoutManager.createLayoutFrame({
name: "ComponentCard",
direction: "VERTICAL",
padding: 24,
itemSpacing: 16,
fills: [{ type: "SOLID", color: COLORS.WHITE }],
cornerRadius: 12,
stroke: { color: COLORS.STROKE, weight: 1 },
effects: [{
type: "DROP_SHADOW",
offset: { x: 0, y: 4 },
radius: 12,
color: { r: 0, g: 0, b: 0, a: 0.1 }
}]
});
The old way:
const card = figma.createFrame();
card.name = "ComponentCard";
card.layoutMode = "VERTICAL";
card.primaryAxisSizingMode = "AUTO";
card.counterAxisSizingMode = "AUTO";
card.itemSpacing = 16;
card.paddingTop = 24;
card.paddingRight = 24;
card.paddingBottom = 24;
card.paddingLeft = 24;
card.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }];
card.cornerRadius = 12;
card.strokes = [{ type: "SOLID", color: { r: 0.9, g: 0.9, b: 0.9 } }];
card.strokeWeight = 1;
card.strokeAlign = "INSIDE";
card.effects = [{
type: "DROP_SHADOW",
visible: true,
radius: 12,
color: { r: 0, g: 0, b: 0, a: 0.1 },
offset: { x: 0, y: 4 },
spread: 0,
blendMode: "NORMAL"
}];
The old way wasn't just longer - I constantly forgot to set primaryAxisSizingMode
or mixed up padding properties.
Dynamic Documentation Generation
The coolest part was generating docs from data:
function generateTokenDocs(tokenData: TokenGroup[]) {
const container = FigmaLayoutManager.createContainer("GeneratedDocs", "VERTICAL");
tokenData.forEach(group => {
// Group header
const groupHeader = FigmaLayoutManager.createGroupLabelRow({
name: group.name,
count: group.tokens.length.toString()
});
// Token grid with wrapping
const tokenGrid = FigmaLayoutManager.createLayoutFrame({
name: "TokenGrid",
direction: "HORIZONTAL",
wrap: true,
itemSpacing: 12,
counterAxisSpacing: 16
});
group.tokens.forEach(token => {
const tokenCard = FigmaLayoutManager.createTokenFrame({
value: token.value,
alias: token.reference,
useAlias: !!token.reference
});
FigmaLayoutManager.addChild(tokenGrid, tokenCard);
});
FigmaLayoutManager.addChild(container, groupHeader);
FigmaLayoutManager.addChild(container, tokenGrid);
});
return container;
}
This kind of stuff would have been impossible with vanilla Figma API. Way too much boilerplate.
How This Transformed Kigen Development
Without FigmaLayoutManager (and my other utilities like FigmaStyleManager and CollectionManager), Kigen would have been:
- 500K+ lines instead of 400K - a 25% increase just from boilerplate
- Much harder to maintain - lots of repetitive code to manage
- More prone to copy-paste bugs - easy to mess up all that repetitive code
- Slower to iterate - changing designs would have meant updating thousands of lines
Building these utilities took time upfront, but they made the difference between building Kigen efficiently and struggling with endless boilerplate.
The One Thing That Mattered
Start with what you wish you could write, then figure out how to make it work. I didn't try to make Figma's API slightly better. I wrote the code I wished existed, then built the wrapper to make it real. This backwards approach saved my project and probably my sanity.
When you're building something as big as Kigen, utilities like this aren't just nice-to-haves. They're essential tools for managing complexity and staying productive.
Kigen is Live!

Kigen
Finally, Kigen is out and people are loving it! The response has been amazing. I've been able to add new features and, thanks to FigmaLayoutManager
, I just updated the documentation layouts from v1 to v2 with gorgeous new designs. The community response has been fantastic.

Kigen Documentation v2
If you want to see FigmaLayoutManager
in action, go give Kigen a try - it's free to use now and you can experience firsthand how these layout utilities made complex documentation generation possible.
Sometimes the best code you write is the code that lets you write less code. FigmaLayoutManager might not be the flashiest part of Kigen, but it's definitely one of the most important for keeping development efficient.