본문으로 건너뛰기

Kiwi Schema for .fig Format

Overview

Kiwi is a schema-based binary encoding protocol created by Evan Wallace. It's similar to Protocol Buffers but simpler and more lightweight. Figma uses Kiwi to encode their .fig file format and clipboard payloads.

Schema Definition

The Kiwi schema defines the structure of all node types, properties, and data structures used in Figma files. The schema includes:

  • Enums - NodeType, BlendMode, PaintType, TextCase, etc.
  • Structs - Fixed-size data structures
  • Messages - Variable-size data structures with optional fields

Schema Reference

The complete Kiwi schema definition for the .fig format is maintained in the repository:

fig.kiwi - Complete Kiwi schema definition

Note: This permalink is a snapshot from December 2025. The schema may evolve as Figma updates their format. For the latest version, see /.ref/figma/fig.kiwi.

This schema is extracted from real .fig files using our fig2kiwi.ts tool.

Key Schema Elements

Node Types

The schema defines over 50 node types, including:

  • Basic: DOCUMENT, CANVAS, FRAME, GROUP
  • Shapes: VECTOR, STAR, LINE, ELLIPSE, RECTANGLE, REGULAR_POLYGON, ROUNDED_RECTANGLE, BOOLEAN_OPERATION
  • Content: TEXT, INSTANCE, SYMBOL, SLICE
  • Modern: SECTION, SECTION_OVERLAY, WIDGET, CODE_BLOCK, TABLE, TABLE_CELL
  • Variables: VARIABLE, VARIABLE_SET, VARIABLE_OVERRIDE
  • Slides: SLIDE, SLIDE_GRID, SLIDE_ROW
  • Code: CODE_COMPONENT, CODE_INSTANCE, CODE_LIBRARY, CODE_FILE, CODE_LAYER
  • Other: STICKY, SHAPE_WITH_TEXT, CONNECTOR, STAMP, MEDIA, HIGHLIGHT, WASHI_TAPE, ASSISTED_LAYOUT, INTERACTIVE_SLIDE_ELEMENT, MODULE, RESPONSIVE_SET, TEXT_PATH, BRUSH, MANAGED_STRING, TRANSFORM, CMS_RICH_TEXT, REPEATER, JSX, EMBEDDED_PROTOTYPE, REACT_FIBER, RESPONSIVE_NODE_SET, WEBPAGE, KEYFRAME, KEYFRAME_TRACK, ANIMATION_PRESET_INSTANCE

Paint Types

  • SOLID - Solid color fill
  • GRADIENT_LINEAR, GRADIENT_RADIAL, GRADIENT_ANGULAR, GRADIENT_DIAMOND
  • IMAGE, VIDEO, PATTERN, NOISE

Effect Types

  • DROP_SHADOW, INNER_SHADOW
  • BACKGROUND_BLUR, FOREGROUND_BLUR
  • GRAIN, NOISE, GLASS

Layout & Constraints

  • LayoutGridType, LayoutGridPattern
  • ConstraintType - MIN, CENTER, MAX, STRETCH, SCALE
  • LayoutMode - NONE, HORIZONTAL, VERTICAL
  • Auto-layout properties with padding, spacing, and alignment

Studied Properties

Properties we've analyzed and documented from the Kiwi schema:

PropertyTypeLocationPurposeUsage
parentIndexParentIndexNodeChange.parentIndexParent-child relationship and orderingContains guid (parent reference) and position (fractional index for ordering)
parentIndex.positionstringParentIndex.positionFractional index string for orderingLexicographically sortable string (e.g., "!", "Qd&", "QeU")
sortPositionstring?NodeChange.sortPositionAlternative ordering fieldTypically undefined for CANVAS nodes, may be used for other node types
frameMaskDisabledboolean?NodeChange.frameMaskDisabledFrame clipping mask settingtrue = clipping disabled (no clip), false = clipping enabled (with clip), undefined = default (clipping enabled). false for GROUP-originated FRAMEs, true for regular FRAMEs without clipping
resizeToFitboolean?NodeChange.resizeToFitAuto-resize to fit contenttrue for GROUP-originated FRAMEs, undefined for real FRAMEs
fillPaintsPaint[]?NodeChange.fillPaintsFill paint arrayEmpty/undefined for GROUPs, may exist for FRAMEs (used in GROUP detection)
strokePaintsPaint[]?NodeChange.strokePaintsStroke paint arrayEmpty/undefined for GROUPs, may exist for FRAMEs (used in GROUP detection)
backgroundPaintsPaint[]?NodeChange.backgroundPaintsBackground paint arrayEmpty/undefined for GROUPs, may exist for FRAMEs (used in GROUP detection)
isStateGroupboolean?NodeChange.isStateGroupIndicates state group/component settrue for component set FRAMEs, undefined for regular FRAMEs
componentPropDefsComponentPropDef[]?NodeChange.componentPropDefsComponent property definitionsPresent on component set FRAMEs, defines variant properties
stateGroupPropertyValueOrdersStateGroupPropertyValueOrder[]?NodeChange.stateGroupPropertyValueOrdersVariant property value ordersPresent on component set FRAMEs, defines order of variant values
variantPropSpecsVariantPropSpec[]?NodeChange.variantPropSpecsVariant property specificationsPresent on SYMBOL nodes that are part of component sets, absent on standalone SYMBOLs

parentIndex

Structure:

interface ParentIndex {
guid: GUID; // Parent node's GUID
position: string; // Fractional index string for ordering
}

Key Finding: CANVAS nodes (pages) use parentIndex.position for ordering, not sortPosition.

Usage:

  • Page Ordering: CANVAS nodes use parentIndex.position to determine their order within the document
  • Child Ordering: All child nodes use parentIndex.position to determine their order within their parent
  • Parent Reference: The guid field references the parent node's GUID

Fractional Index Strings:

Figma uses fractional indexing (also known as "orderable strings") for maintaining order in collaborative systems:

  • Allows insertion between items without renumbering
  • Strings are designed to sort correctly when compared lexicographically
  • Examples: "!", " ~\", "Qd&", "QeU", "Qe7", "QeO", "Qf", "Qi", "Qir"
  • These are not numeric values - they're special strings optimized for lexicographic sorting

Implementation:

// Sort pages by parentIndex.position
const sortedPages = canvasNodes.sort((a, b) => {
const aPos = a.parentIndex?.position ?? "";
const bPos = b.parentIndex?.position ?? "";
return aPos.localeCompare(bPos); // Lexicographic comparison
});

// Sort children by parentIndex.position
const sortedChildren = children.sort((a, b) => {
const aPos = a.parentIndex?.position ?? "";
const bPos = b.parentIndex?.position ?? "";
return aPos.localeCompare(bPos);
});

Important: Always use lexicographic (string) comparison with localeCompare(). Never try to parse these as numbers - the strings are already in the correct format for sorting.

sortPosition

Type: string | undefined

Location: NodeChange.sortPosition

Usage: The sortPosition field exists on NodeChange but is typically undefined for CANVAS nodes. It may be used for other node types or specific contexts. For page ordering, use parentIndex.position instead.

GROUP vs FRAME Detection

Critical Finding: Figma converts GROUP nodes to FRAME nodes in both clipboard payloads and .fig files. This means:

  • No GROUP node type exists in parsed data - all groups are stored as FRAME nodes
  • The original group name is preserved in the name field
  • We can detect GROUP-originated FRAMEs using specific property combinations

Detection Properties:

PropertyReal FRAMEGROUP-originated FRAMEReliability
frameMaskDisabledtruefalse✅ Reliable
resizeToFitundefinedtrue⚠️ Check with paints
fillPaintsMay existundefined or []✅ Safety check
strokePaintsMay existundefined or []✅ Safety check
backgroundPaintsMay existundefined or []✅ Safety check

Note on frameMaskDisabled semantics:

  • frameMaskDisabled: true = clipping is disabled (no clip)
  • frameMaskDisabled: false = clipping is enabled (with clip)
  • frameMaskDisabled: undefined = default behavior (clipping enabled)

Observed values:

  • Regular FRAMEs (without clipping) typically have frameMaskDisabled: true (clipping disabled)
  • FRAMEs with clipping enabled have frameMaskDisabled: false (clipping enabled)
  • GROUP-originated FRAMEs have frameMaskDisabled: false (but can be distinguished by resizeToFit: true and lack of paints)
  • When frameMaskDisabled is undefined, the default behavior is clipping enabled (maps to clipsContent: true)

Note: The property name is counterintuitive - frameMaskDisabled: true means the mask (clipping) is disabled, not that the frame is disabled.

Detection Logic:

function isGroupOriginatedFrame(node: NodeChange): boolean {
if (node.type !== "FRAME") {
return false;
}

// Primary indicators
if (node.frameMaskDisabled !== false || node.resizeToFit !== true) {
return false;
}

// Additional safety check: GROUPs have no paints
// (GROUPs don't have fills or strokes, so this is an extra safeguard)
const hasNoFills = !node.fillPaints || node.fillPaints.length === 0;
const hasNoStrokes = !node.strokePaints || node.strokePaints.length === 0;
const hasNoBackgroundPaints =
!node.backgroundPaints || node.backgroundPaints.length === 0;

return hasNoFills && hasNoStrokes && hasNoBackgroundPaints;
}

Note: The paint checks (fillPaints, strokePaints, backgroundPaints) are used as additional safety checks since we can't be 100% confident in relying solely on resizeToFit. GROUPs never have fills or strokes, so this provides extra confidence in the detection.

Verification:

This behavior has been verified in:

  • Clipboard payloads (see fixtures/test-fig/clipboard/group-with-r-g-b-rect.clipboard.html)
  • .fig files (see fixtures/test-fig/L0/frame.fig)

Both formats show the same pattern: GROUP nodes are stored as FRAME nodes with distinguishing properties.

Implementation Notes:

When converting from Figma to Grida:

  1. Check if a FRAME node has GROUP-like properties
  2. If detected, convert to GroupNode instead of ContainerNode
  3. This ensures proper semantic mapping: GROUP → GroupNode, FRAME → ContainerNode

Component Sets

Critical Finding: There is no COMPONENT_SET node type in the Kiwi schema. Component sets are represented as:

  • A FRAME node (the component set container)
  • Containing multiple SYMBOL nodes as children (the component variants)

Component Set FRAME Properties:

A FRAME that is a component set has these distinguishing properties:

PropertyComponent Set FRAMERegular FRAMEReliability
isStateGrouptrueundefined✅ Reliable
componentPropDefsPresentundefined✅ Reliable
stateGroupPropertyValueOrdersPresentundefined✅ Reliable

Component Set SYMBOL Properties:

A SYMBOL that is part of a component set has:

PropertyComponent Set SYMBOLStandalone SYMBOLReliability
variantPropSpecsPresentundefined✅ Reliable

Structure:

DOCUMENT "Document"
└─ CANVAS "Internal Only Canvas" (component library)
└─ FRAME "Button" (component set container)
├─ SYMBOL "Variant=Primary, State=Default, Size=Small"
├─ SYMBOL "Variant=Neutral, State=Default, Size=Small"
└─ ... (more SYMBOL variants)

Detection Logic:

// Detect component set FRAME
function isComponentSetFrame(node: NodeChange): boolean {
if (node.type !== "FRAME") {
return false;
}
return (
node.isStateGroup === true &&
node.componentPropDefs !== undefined &&
node.componentPropDefs.length > 0
);
}

// Detect component set SYMBOL
function isComponentSetSymbol(node: NodeChange): boolean {
if (node.type !== "SYMBOL") {
return false;
}
return (
node.variantPropSpecs !== undefined && node.variantPropSpecs.length > 0
);
}

Verification:

This structure has been verified in:

  • Clipboard payloads (see fixtures/test-fig/clipboard/component-set-cards.clipboard.html)
  • .fig files (see fixtures/test-fig/L0/components.fig)

Both formats show the same pattern: component sets are FRAME nodes containing SYMBOL children, with distinguishing properties on both the FRAME and SYMBOL nodes.

Component sets in clipboard payloads (observed)

Clipboard payloads add some practical patterns around where the component-set FRAME and variant SYMBOL nodes appear:

  • Copying the component set container (see fixtures/test-fig/clipboard/component-component-set.clipboard.html):

    • The user-facing canvas contains the component-set FRAME (isStateGroup === true) with variant SYMBOL children.
    • An "Internal Only Canvas" (CANVAS.internalOnly === true) may still be present.
  • Copying a variant component itself (see fixtures/test-fig/clipboard/component-component-set-component-*.clipboard.html):

    • The user-facing canvas contains the copied variant as a SYMBOL.
    • The internal-only canvas contains the component-set FRAME and its variant SYMBOL children.
  • Copying a variant instance (see fixtures/test-fig/clipboard/component-component-set-component-instance-*.clipboard.html):

    • The user-facing canvas contains an INSTANCE.
    • The internal-only canvas contains the component-set FRAME and its variant SYMBOL children.
    • The reference uses INSTANCE.symbolData.symbolIDSYMBOL.guid (where the referenced SYMBOL is a variant under the component-set FRAME, not necessarily a direct child of a CANVAS).

Verified in fixtures:

  • fixtures/test-fig/clipboard/component-component-set.clipboard.html
  • fixtures/test-fig/clipboard/component-component-set-component-blue.clipboard.html
  • fixtures/test-fig/clipboard/component-component-set-component-red.clipboard.html
  • fixtures/test-fig/clipboard/component-component-set-component-instance-blue.clipboard.html
  • fixtures/test-fig/clipboard/component-component-set-component-instance-red.clipboard.html

Components & Instances (clipboard payloads)

The Kiwi clipboard payloads in fixtures/test-fig/clipboard demonstrate how components and instances are represented in practice.

Node types

  • Component definition: SYMBOL
  • Component instance: INSTANCE

Internal-only canvas

Clipboard payloads may include a canvas commonly named "Internal Only Canvas" where:

  • The CANVAS.internalOnly field is true
  • Component definitions (SYMBOL) may be stored there, even when the user copies an INSTANCE

Observed patterns (from fixtures)

When copying the component definition (component-component-*.clipboard.html):

  • The SYMBOL appears under the user-facing canvas (e.g. "Page 1").
  • An "Internal Only Canvas" may still be present, but is empty of SYMBOL children in these fixtures.

When copying a component instance (component-component-instance-*.clipboard.html):

  • The user-facing canvas contains an INSTANCE.
  • The "Internal Only Canvas" contains the referenced SYMBOL definition.
  • The linkage is INSTANCE.symbolData.symbolIDSYMBOL.guid.

When copying a component-set variant instance (component-component-set-component-instance-*.clipboard.html):

  • The user-facing canvas contains an INSTANCE.
  • The internal-only canvas contains the component set FRAME (isStateGroup === true) and its variant SYMBOL children.
  • The linkage is still INSTANCE.symbolData.symbolIDSYMBOL.guid (the referenced SYMBOL is a variant under the component-set FRAME).

Verified in fixtures:

  • fixtures/test-fig/clipboard/component-component-blue.clipboard.html
  • fixtures/test-fig/clipboard/component-component-red.clipboard.html
  • fixtures/test-fig/clipboard/component-component-instance-blue.clipboard.html
  • fixtures/test-fig/clipboard/component-component-instance-red.clipboard.html
  • fixtures/test-fig/clipboard/component-component-set-component-instance-blue.clipboard.html
  • fixtures/test-fig/clipboard/component-component-set-component-instance-red.clipboard.html

Vector

Node Type: VECTOR

VECTOR nodes represent vector graphics (paths/shapes) in Figma. The vector geometry is stored in a binary format within the .fig file.

Vector network coordinate space (observed)

When VECTOR.vectorData.vectorNetworkBlob is present, the decoded vector network coordinates (vertices and segment tangents) are not always expressed in the node’s size coordinate space.

  • The vector network coordinates are typically expressed in the vectorData.normalizedSize coordinate space (in observed real-world .fig data, many blob vertex bboxes match normalizedSize closely).
  • The node’s rendered size is represented by NodeChange.size.
  • To map the vector network into the node’s local size space, you generally need to scale:
    • sx=size.xnormalizedSize.xs_x = \frac{\text{size.x}}{\text{normalizedSize.x}}
    • sy=size.ynormalizedSize.ys_y = \frac{\text{size.y}}{\text{normalizedSize.y}}
  • This scaling applies to both:
    • vertex positions (x, y)
    • segment tangents (dx, dy)

Practical consequence: treating vector network coordinates “as-is” (without accounting for normalizedSize vs size) can produce vectors that render at the wrong size and appear mis-positioned relative to their container.

Caveat (also observed): some blobs have non-zero bbox origins (e.g. minX/minY not exactly 0), so in some cases an additional translation may be necessary beyond pure scaling.

VectorData Structure:

VECTOR nodes contain a vectorData field of type VectorData:

FieldTypeDescription
vectorNetworkBlobnumber?Blob ID referencing binary vector network data
normalizedSizeVector?Normalized size (x, y)
styleOverrideTableNodeChange[]?Style overrides

Vector Network Blob Format:

The vectorNetworkBlob field contains a blob ID (number) that references binary data stored in the message's blobs array. This binary data encodes the vector network in a specific little-endian format:

Header (12 bytes total):

FieldTypeOffsetDescription
vertexCountu320Number of vertices
segmentCountu324Number of segments
regionCountu328Number of regions

Vertices (12 bytes each):

FieldTypeOffsetDescription
styleIDu320Style identifier for the vertex
xf324X coordinate
yf328Y coordinate

Segments (28 bytes each):

FieldTypeOffsetDescription
styleIDu320Style identifier for the segment
startVertexu324Index of the start vertex
start.dxf328Start tangent X component
start.dyf3212Start tangent Y component
endVertexu3216Index of the end vertex
end.dxf3220End tangent X component
end.dyf3224End tangent Y component

Regions:

FieldTypeDescription
styleID+windingRuleu32Style ID (bits 1-31) and winding rule (bit 0: 0=ODD, 1=NONZERO)
loopCountu32Number of loops in this region
loopsLoop[]Array of loops, where each loop contains:
loops[].indexCountu32Number of segment indices in this loop
loops[].indicesu32[]Array of segment indices forming the closed loop

Parsed VectorNetwork Structure:

After parsing, the binary blob is converted to a structured VectorNetwork object:

FieldTypeDescription
verticesArray<{ styleID: number; x: number; y: number }>Array of vertex positions and style IDs
segmentsArray<{ styleID: number; start: { vertex: number; dx: number; dy: number }; end: { vertex: number; dx: number; dy: number } }>Array of segments connecting vertices with tangent handles
regionsArray<{ styleID: number; windingRule: "NONZERO" | "ODD"; loops: Array<{ segments: number[] }> }>Array of regions defining closed shapes

Parsing Example:

// Get the blob bytes from the message
const blobBytes = getBlobBytes(vectorData.vectorNetworkBlob, message);

// Parse the vector network
const vectorNetwork = parseVectorNetworkBlob(blobBytes);
// Returns: {
// vertices: [{ styleID: number, x: number, y: number }],
// segments: [{ styleID: number, start: { vertex: number, dx: number, dy: number }, end: { vertex: number, dx: number, dy: number } }],
// regions: [{ styleID: number, windingRule: "NONZERO" | "ODD", loops: [{ segments: number[] }] }]
// }

Key Points:

  • The vector network uses a graph-based representation with vertices, segments (edges with tangent handles), and regions (closed loops of segments)
  • Segments connect vertices and include tangent handle information (dx, dy) for curved paths
  • Regions define closed shapes using loops of segment indices
  • Winding rules determine fill behavior: NONZERO or ODD (even-odd)
  • Style IDs reference styles from the style system for fills, strokes, and effects
  • The normalizedSize field provides the coordinate space dimensions for the vector

External Resources