Pixel-accurate Figma β Roblox UI exporter using the PNG-slice pipeline.
Extracts any Figma frame and generates a production-ready .rbxmx file: every visual element becomes a PNG ImageLabel, dynamic text becomes TextLabel, and layout hierarchy is preserved as nested Frame containers. Drop shadows, gradients, strokes, and effects are all baked into the PNGs β zero approximation, pixel-perfect results.
Every Layer Sliced β FigmaForge classifies each node as one of three types:
| Classification | Roblox Instance | Logic |
|---|---|---|
| PNG | ImageLabel |
Any leaf node or subtree without dynamic text β rasterized via exportAsync |
| Dynamic Text | TextLabel |
Text nodes with $ prefix or matching dynamic patterns (price, level, etc.) |
| Container | Frame |
Parent nodes with dynamic text descendants β preserves hierarchy, self rasterized as background |
Complex Figma features (radial gradients, blurs, complex strokes, shadows) all "just work" because they're baked into the PNG.
Figma Desktop (MCP Bridge plugin)
β figma_execute β extraction script runs in Figma sandbox
JSON Manifest (IR with node tree + base64 PNGs)
β figma-forge-cli.ts --resolve-images
Pipeline: dedup β classify β upload PNGs β assemble .rbxmx β Rojo safety check
β
.rbxmx + .bindings.json βββ Rojo auto-sync βββ Roblox Studio
| Module | Purpose |
|---|---|
figma-forge-ir.ts |
TypeScript IR interfaces β node tree, fills, strokes, text, effects, _renderBounds |
figma-forge-extract.ts |
Builds the JS extraction script for Figma sandbox β node serialization, render bounds, layer classification, text-stroke deduplication |
figma-forge-assemble.ts |
.rbxmx XML generator β node classification, positioning, ImageLabel/TextLabel/Frame emission, auto-layout β UIListLayout |
figma-forge-images.ts |
Image upload pipeline β base64 PNG β Roblox Open Cloud API β rbxassetid://, content-hash caching |
figma-forge-shared.ts |
Font mapping (FigmaβRoblox), dynamic text classification (SSOT), scroll detection, config compilation, XML/Lua escaping |
figma-forge-cli.ts |
CLI orchestrator β arg parsing, config loading, Rojo safety validator, binding manifest generation |
figma-forge-bindings.ts |
Binding manifest generator β walks IR tree to detect buttons, tabs, templates, text bindings, scroll containers |
figma-forge-export.ts |
Batch PNG export script generator β chunked exportAsync for Figma's 30s timeout |
figma-forge-diff.ts |
Incremental re-export β structural hash diffing, --incremental support, saves ~80% upload time |
figma-forge-animations.ts |
Prototype transition β TweenService Luau code generation |
figma-forge-kit.ts |
UI Kit page extraction β component sets with variants β Lua Kit module with state switching |
- Node.js 18+
- Figma Desktop with the MCP Desktop Bridge plugin running
- Rojo serving your project (for
.rbxmxauto-sync) - Roblox Open Cloud API key (see Image Pipeline section)
cd tools/FigmaForge
npm install
npx tsc --outDir distnpx ts-node figma-forge-cli.ts \
--input manifest.json \
--output ../../src/StarterGui/MyFrame.rbxmx \
--resolve-images \
--api-key YOUR_ROBLOX_API_KEY \
--creator-id YOUR_ROBLOX_CREATOR_ID \
--verboseOptions:
--input, -i Path to FigmaForge manifest JSON (required)
--output, -o Path for generated .rbxmx (default: <input>.rbxmx)
--scale, -s PNG export scale factor (default: 2)
--config, -c Path to custom figmaforge.config.json
--text-export Text export mode: 'all' (default), 'dynamic', 'none'
--resolve-images Upload exported PNGs to Roblox Cloud
--merge-images Path to exported-images JSON to merge into manifest
--api-key Roblox Open Cloud API key (highest priority)
--creator-id Roblox creator/user ID for asset ownership
--skip-dedup Skip text-stroke deduplication pass
--responsive Use scale-based sizing proportional to root
--incremental <prev> Path to previous manifest for incremental re-export
--save-manifest Save current state for future --incremental exports
--verbose, -v Show detailed processing info
--help, -h Show help
The CLI produces two files:
<name>.rbxmxβ the Roblox UI tree (auto-synced via Rojo)<name>.bindings.jsonβ binding manifest listing buttons, text bindings, templates, tabs, scroll containers
When extraction identifies PNG nodes, they're rasterized via exportAsync at 2Γ scale. The CLI uploads them to Roblox Cloud and patches rbxassetid:// URIs into the .rbxmx.
Config priority for Roblox API credentials:
- CLI arguments (recommended) β
--api-key YOUR_KEY --creator-id YOUR_ID - Environment variables:
ROBLOX_API_KEY+ROBLOX_CREATOR_ID .envfile in FigmaForge directoryscripts/roblox-config.jsonβ project-level fallback
Warning
Always clear stale env vars before running CLI: $Env:ROBLOX_API_KEY=$null; $Env:ROBLOX_CREATOR_ID=$null
FigmaForge is genre-agnostic. Override default text and button detection heuristics:
{
"dynamicPrefix": "$",
"textExportMode": "dynamic",
"dynamicNamePatterns": [
"^price", "^level", "^score", "^amount"
],
"dynamicTextPatterns": [
"^\\\\{[^}]+\\\\}$",
"^[\\\\d,]+$"
],
"interactivePatterns": [
"btn", "button", "tab_"
]
}textExportMode(config default:"dynamic", CLI default:"all"):"all": Every text node becomes a RobloxTextLabel."dynamic": Only text nodes matching the dynamic patterns becomeTextLabels. All other text is baked into the rasterized PNG background."none": All text is baked into the background PNG.
Uploaded images are cached by content hash in .figmaforge-image-cache.json. Re-exports reuse existing rbxassetid:// URIs. Delete the cache file to force re-uploads.
Important
This is a critical architectural detail for pixel-perfect exports.
Figma's exportAsync() renders at absoluteRenderBounds (includes effects like drop shadows, blurs), but the node's .width/.height properties only report the logical bounding box. Without correction, PNGs with shadow padding get squeezed into too-small ImageLabels.
FigmaForge handles this automatically:
- Extraction (
figma-forge-extract.ts): ComparesabsoluteRenderBoundsvsabsoluteBoundingBoxfor nodes with visible effects or strokes. Stores the delta as_renderBoundsin the IR. - Assembly (
figma-forge-assemble.ts): Uses_renderBoundsfor ImageLabel position and size when present, falling back to standardx/y/width/heightotherwise.
The assembler (figma-forge-assemble.ts β classifyNode) determines how each IR node is emitted:
| Criterion | Classification | Output |
|---|---|---|
| Text matching config rules / export mode | text_dynamic |
TextLabel |
| Has children with dynamic text descendants | container |
Frame (with background ImageLabel if hybrid) |
| Everything else (leaf, no dynamic children) | png |
ImageLabel |
Nodes with a single solid fill (no strokes, no gradients) skip rasterization entirely β they're emitted as native Roblox Frame with BackgroundColor3, avoiding unnecessary PNG uploads.
FigmaForge recognizes special suffixes in Figma layer names:
| Suffix | Effect |
|---|---|
[Flatten] / [Raster] |
Force-rasterize entire subtree as one PNG |
[Template] |
Mark as template node for dynamic list cloning |
[Scroll] |
Force ScrollingFrame output |
$ prefix |
Force dynamic text classification (TextLabel) |
Designers can annotate nodes via:
- Name suffix:
BulletPoint[Template],ContentPane[Scroll] - Name pattern:
*Btnβ button,Tab_*β tab,$Priceβ dynamic text - Figma description:
@template,@scroll,@bind:key,@button,@tab
The CLI validates the generated .rbxmx before writing, catching:
<token>tags for properties Rojo expects as<int>(AutomaticSize, ScrollBarThickness, etc.)- Mismatched
<Item>open/close tags (XML well-formedness) - Duplicate referent IDs (causes Rojo to silently merge nodes)
Build fails fast with actionable error messages if any check fails.
- Roblox font mapping β Figma fonts mapped to closest Roblox equivalent (InterβBuilderSans). Some fonts may not have exact matches.
- Per-corner radius β Roblox
UICorneronly supports uniform radius. Per-corner is approximated with max value. SPACE_BETWEENlayout β No Roblox equivalent, falls back toMINalignment.- Component instances β Exported as their expanded tree, not as Roblox component references.
| Symptom | Cause | Fix |
|---|---|---|
Rojo: invalid digit found in string |
Negative value in <token> tag |
Tokens must be unsigned ints (0+) |
Rojo: duplicate referent |
Two nodes share same referent ID | Check assembler assigns unique referents |
| Empty/white ImageLabels | Unresolved image hashes | Run with --resolve-images |
| Squished buttons/shadows | Missing render bounds | Ensure extraction captures absoluteRenderBounds |
[Flatten] in node name |
Rojo interprets brackets | Post-process: strip [Flatten] tags from .rbxmx |
| Rojo won't re-sync destroyed instance | $ignoreUnknownInstances: true |
Disconnect and reconnect Rojo plugin |
| Images fail to load | Stale image cache | Delete .figmaforge-image-cache.json and re-run |
| Dark/squished text | Static text rasterized as PNG | Check textExportMode β use all to keep all text as TextLabel |
MIT License β see LICENSE for details.
