Fast .fig tooling for Elixir. Analyze production-sized Figma files, transform them with ordinary Elixir, and render pages or individual layers on the server.
Figler parses .fig files faster than Figma's JavaScript implementation while giving you structs, pattern matching, Enum, XPath-style queries, and headless Skia rendering inside the BEAM.
fig = File.read!("design.fig")
index = Figler.Scene.index(fig)
buttons = Figler.Scene.name_contains(index, "Button")
copy = Figler.Scene.text_contains(index, "Checkout")
{:ok, png, %{warnings: []}} =
Figler.Render.render(fig, root: "12:34", scale: 2, strict: true)
File.write!("button.png", png)No Node.js process, browser session, REST export pipeline, or conversion to generic JSON.
Figma's own file tooling is built around JavaScript. Analyzing a large design from Elixir normally means running another runtime, converting the file, or sending it to an external service.
Figler reads the original .fig file directly. Build an index once, then inspect pages, components, text, hierarchy, geometry, and styles with small, focused queries. Large files stay practical for background jobs, design-system checks, asset pipelines, migrations, and automated exports.
When you need to edit a file, decode it into generated Elixir structs and use the language you already know. Pattern matching and Enum are the transformation API. Encoding rebuilds the original representation and preserves untouched archive entries, metadata, images, and Kiwi container data.
Rendering is part of the same library. Figler resolves components, instances, overrides, variables, layout, vectors, paints, effects, masks, images, and rich text before compiling the selected root into one batched Skia document.
See Why Figler for common use cases and the design behind the API.
def deps do
[
{:figler, "~> 0.1.0-beta.1"},
{:skia, "~> 0.3.7"} # optional, only needed for rendering
]
endFigler requires Elixir 1.19 or later and a stable Rust toolchain to build its native parser. Rendering uses Skia's published native artifacts when available.
See Getting Started.
Build an index once and run as many queries as you need:
alias Figler.Scene
index = "design.fig" |> File.read!() |> Scene.index()
pages = Scene.pages(index)
components = Scene.components(index)
frames = Scene.by_type(index, :frame, fields: [:name, :size])
buttons = Scene.name_contains(index, "Button")
labels = Scene.text_contains(index, "Continue", fields: [:name, :text])Use XPath-style selectors for structural questions:
visible_text =
Scene.query(index, "//text[@visible='true'][@text]",
fields: [:name, :text, :parent_guid]
)
cards = Scene.query(index, "//frame[contains(@name, 'Card')]")Navigate through the document without rebuilding it:
node = Scene.node(index, "12:34")
parent = Scene.parent(index, "12:34")
children = Scene.children(index, "12:34")
descendants = Scene.descendants(index, "12:34")See Querying Scenes.
Decode the complete file, change ordinary Elixir structs, and encode it again:
alias Figler.Schema.NodeChange
fig = File.read!("design.fig")
document = Figler.decode!(fig)
document =
update_in(document.message.node_changes, fn nodes ->
Enum.map(nodes, fn
%NodeChange{text_data: %{characters: "Buy now"} = text} = node ->
%{node | text_data: %{text | characters: "Checkout"}}
node ->
node
end)
end)
File.write!("updated.fig", Figler.encode!(document))The same API works for complete .fig archives, standalone fig-kiwi containers, and raw Figma messages. Output keeps the same representation as the input.
Because transformations are just data transformations, you can compose them with normal functions, pattern matching, Enum.map/2, update_in/2, and put_in/2 instead of learning an editing DSL.
See Transforming Files.
Raw file records describe the source document. Effective graphs apply instance population, swaps, component properties, overrides, variables, and layout so you can inspect the resulting design:
graph = Figler.Scene.graph(fig, root: "12:34")
resolved_copy = Figler.Scene.text_contains(graph, "Continue")
tree = Figler.Scene.Graph.tree(graph, root: "12:34", depth: 4)
instance = Figler.Scene.Graph.instance(graph, "12:34")
explanation = Figler.Scene.Graph.explain(graph, "12:34")See Effective Graphs.
Render a page, frame, component, or individual layer without a browser:
document = Figler.Document.open!(fig)
{:ok, png, metadata} =
Figler.Render.render(document,
root: "12:34",
scale: 2,
background: :transparent,
format: :png,
strict: true,
fallback_font_paths: ["priv/fonts/Inter-Regular.ttf"]
)
File.write!("selection.png", png)
metadata.boundsUse strict: true for automated exports. Unsupported features, approximations, missing assets, and font substitutions become an error instead of silently changing the image. Non-strict mode returns the same information as structured warnings alongside the rendered output.
See Headless Rendering, the Rendering Support Matrix, and Rendering Warnings.
0.1.0-beta.1 provides production-shaped analysis, transformation, and rendering APIs, but rendering intentionally supports a documented subset of Figma. It does not promise pixel identity with Figma or OpenPencil for arbitrary documents.
See Compatibility, Performance and Safety, and the Changelog.
Full guides and API documentation are available on HexDocs.
mix deps.get
mix ciMIT © 2026 dannote