Building Figma Layout Manager: Made Kigen Documentation Layout Possible

Jul 22, 2025 · 8 min read

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.

Kigen Plugin - Here check now

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.

stay hungry, stay foolish

-Steve Jobs

©realvjyvijay verma rss