Utilities

Export Icons

How to export icons from Figma components into a project with the Export Icons CLI: install, config, sources and targets, naming, icon font, SF Symbols, Figma Motion and change tracking.

Export Icons (@sxl-studio/export-icons) is a command-line utility that downloads icons from Figma components into a project folder. It exports COMPONENT nodes and every variant of a COMPONENT_SET, remembers what it has already downloaded and on the next run fetches only what changed. From the same set it can also build an icon font and iOS SF Symbols.

What you getDetails
Static filessvg, png, jpg, webp, gif, pdf
Animated icons (Figma Motion)svg, gif, mp4, webm
Icon fontwoff2, woff, ttf, eot, svg with a stylesheet and a preview page
iOSAssets.xcassets with a .symbolset per icon
Change trackingNew, updated, renamed and deleted icons are detected between runs

Quick start

  1. Install the package into the project. Node.js 20.9 or newer is required.
  2. Put the Figma token and the file key into .env (see Access to Figma below).
  3. Create a config with the wizard: it asks a few questions and writes a valid file.
  4. Check the config and preview the changes with --dry-run.
  5. Run the sync. If the folder already contains icons downloaded earlier, add --adopt-existing to the first run: the files are accepted as the baseline instead of being downloaded again.
BASH
npm install --save-dev @sxl-studio/export-icons
pnpm exec sxl-export-icons init --wizard
pnpm exec sxl-export-icons validate-config --config sxl-export-icons.config.yaml
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --dry-run
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --adopt-existing
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml

With pnpm the installed binary runs as pnpm exec sxl-export-icons, with npm as npx sxl-export-icons. For a one-shot run without installing use npx @sxl-studio/export-icons or pnpm dlx @sxl-studio/export-icons. Do not use pnpx sxl-export-icons: in private registries it may resolve the unscoped package sxl-export-icons and fail with 404.

When to use

  • Icons live in a Figma library and must land in the repository as files, with stable names and without manual export.
  • The same icons are needed in several formats or sizes, for example SVG for web and WebP previews.
  • An icon font or SF Symbols must be built from the same set.
  • Icons change often, and you want to download only what changed, from a terminal or in CI.

Access to Figma

  1. Open Figma account settings and create a personal access token with the scopes file_content:read and file_metadata:read. Write scopes are not needed. Figma tokens expire, so choose a lifetime that suits your policy.
  2. Take the file key from the file URL: https://www.figma.com/design/<fileKey>/....
  3. Save both in .env at the repository root, and add .env and .env.local to .gitignore.
BASH
FIGMA_TOKEN=figd_xxxxxxxxxxxxxxxxx
SXL_ICONS_FILE_KEY=xxxxxxxxxxxx
# only for Bridge: mode: mcp, Figma Motion, plugin transport
BRIDGE_AUTH_TOKEN=xxxxxxxxxxxx

The CLI reads .env.local and .env from the current folder and from all parent folders. The variable names can be changed in the config: env.figmaTokenVar, sources[].fileKeyVar, bridge.authTokenVar.

Commands

CommandWhat it does
sync (default)Reads the sources, downloads new and changed icons, removes deleted ones, then builds the font and SF Symbols
initWrites the starter sxl-export-icons.config.yaml; with --wizard asks questions instead of copying the template
validate-configChecks the YAML, the paths and the environment variables
reportPrints, per source and target, how many icons are tracked and whether their files exist
cleanDeletes the files listed in the state and clears the state
probe-motionShows what Figma reports for a Motion node, without writing files
help, versionHelp and the package version

create-config is an alias of init, init-wizard of init --wizard. Without --config the CLI looks in the current folder for sxl-export-icons.config.yaml, icons.config.yaml, config/sxl-export-icons.config.yaml and config/icons.config.yaml.

Flags of sync

FlagWhat it does
--config <path>Config file; repeat the flag to run several configs at once
--config-glob "<glob>"Configs by pattern, for example "packages/*/sxl-export-icons.config.yaml"
--target <id>Run only the listed targets; repeatable
--dry-runShows what would change and writes nothing
--fullClears the state first and exports everything again. Run it with all targets: with --target the files of the other targets stay on disk, but their next sync starts from an empty state and reports them as new
--adopt-existingOn the first run accepts the files already on disk as unchanged instead of downloading them again
--allow-fallback-restIf a mcp source is unavailable, reads it through the REST API when the source has a file key
--report <path>Writes a JSON report of the run

Flags of other commands

CommandFlagWhat it does
init--path <path>Where to write the config; default sxl-export-icons.config.yaml
init--forceOverwrite an existing config
init--wizardInteractive questions; needs a real terminal
clean--dry-runOnly list what would be deleted
clean--skip-auxKeep the font and SF Symbols folders; delete only the tracked icon files
validate-config, report--config, --config-globSame as for sync
probe-motion--node-id <id>Node to inspect; without it, the current selection in Figma
probe-motion--format <list>Formats to test, comma-separated or repeated: MP4, WEBM, GIF, SVG
probe-motion--inspect-only, --include-descendantsRead the Motion data without a test export; include nested layers (--descendant-max-depth <n> and --descendant-max-nodes <n> limit the traversal)
probe-motion--ignore-overlapping-layers, --include-id-attribute, --bridge-url <url>The matching Figma export options for the SVG test; Bridge address, default http://127.0.0.1:37830

Setup wizard

init --wizard runs only in an interactive terminal. It asks, in order: the language (en or ru), whether to create .env with empty variables, the state file path, whether to allow paths outside the project, the source id, the source mode (rest or mcp), the variable with the file key, the page name, the download speed, the target id, the output path, the format (svg, png, jpg, webp, gif or pdf), the name case, the icon font (path and formats), SF Symbols (path) and the description marker filter. The result is a version: 3 config with the name pattern {prefix}{variant.style}{sectionName}{baseName}{suffix}. For mp4 and webm add motion.enabled: true by hand.

Configuration

The config is a YAML file with version: 3. sources say where icons are read from, targets say where and how they are written. Relative paths are resolved from the config file's folder.

YAML
version: 3
env:
  figmaTokenVar: FIGMA_TOKEN
bridge:
  baseUrl: http://127.0.0.1:37830
  authTokenVar: BRIDGE_AUTH_TOKEN
state:
  file: .sxl/cache/icons-state.json
safety:
  allowOutsideWorkspace: false
sources:
  - id: icons
    mode: rest
    fileKeyVar: SXL_ICONS_FILE_KEY
    pageName: Icons
targets:
  - id: web-svg
    sourceIds: [icons]
    output:
      path: assets/icons/svg
      format: svg
KeyDefaultWhat it does
env.figmaTokenVarFIGMA_TOKENName of the variable with the Figma token
bridge.baseUrlhttp://127.0.0.1:37830Address of Bridge. Used only by mode: mcp sources, Motion targets and plugin transport
bridge.authTokenVarBRIDGE_AUTH_TOKENVariable with the Bridge access token
state.fileassets/.cache/sxl-export-icons-state.jsonWhere the CLI remembers what it exported. The template and the wizard use .sxl/cache/icons-state.json; both are fine, pick one and keep it
safety.allowOutsideWorkspacefalseBy default output and state paths must stay inside the folder where the command runs. true allows writing elsewhere, for example from packages/ds/tokens into packages/ds/assets

A version: 2 config (icons.config.yaml) is still read and migrated on the fly; its relative paths are resolved from the folder where the command runs.

Sources

YAML
sources:
  - id: mono
    mode: rest
    fileKeyVar: SXL_ICONS_FILE_KEY
    pageName: Monochrome
    sectionName: Actions
    downloadSpeed: 20
    descriptionExportMarker:
      key: sxl-studio-export-icon
      includeWhenMissing: true
KeyDefaultWhat it does
idrequiredName used in targets[].sourceIds
moderestrest reads the file through the Figma REST API. mcp reads the icon list from the file open in Figma, through Bridge and Remote Connect; the files themselves are still downloaded through REST, so the token and the file key are still needed
fileKey, fileKeyVarrequired for restThe file key itself or the name of the variable that holds it
pageName, pageIdall pagesRead only this page
sectionName, sectionIdall sectionsRead only the icons inside this section
downloadSpeed10Requests per second to Figma. Lower it when Figma answers 429
descriptionExportMarkeroffFilter by a marker in the component description, see below
selectorsnoneInclude and exclude rules, see below
SelectorWhat it does
includeNodeIds, excludeNodeIdsBy node id
includeComponentNames, excludeComponentNamesBy component name
includeComponentSetNames, excludeComponentSetNamesBy component set name
variantMatchExact match of variant properties. Any property works, not only style; the value is a string or a list of strings
YAML
selectors:
  variantMatch:
    state: [default, hover]
    style: outlined

Selectors also work at the target level (targets[].selectors) and inside an override (overrides[].patch.selectors); there they filter only the icons of that target or rule.

Description marker. Write sxl-studio-export-icon: true or sxl-studio-export-icon: false in the description of a component or component set. true exports the icon, false skips it, and a missing marker follows includeWhenMissing. The key name is configurable.

Targets

YAML
targets:
  - id: web-icons
    mode: sync
    sourceIds: [mono]
    download:
      enabled: true
      pruneUntracked: false
    output:
      path: assets/icons
      format: svg
      scale: 1
      quality: 100
      lossless: false
      unsafeSvgPolicy: fail
    naming:
      case: kebab
      pattern: "{prefix}{variant.style}{sectionName}{baseName}{suffix}"
      collisionStrategy: add-nodeid
KeyDefaultWhat it does
idrequiredTarget name, used with --target
modesyncsync reads Figma and writes files. convert-local takes the SVG files already in output.path and only builds the font or SF Symbols
sourceIdsrequired for syncSources to export
selectorsnoneAdditional filter for this target
download.enabledtruefalse disables download, rename and delete; the local files are used as they are
download.pruneUntrackedfalseAlso delete files of the target's format in output.path that are not in the current Figma set. Use only for folders that belong to this target alone
outputFormat and processing, see the next table
namingFile names, see Naming
overrides, sourceExport, motion, font, sfSymbolsoffDescribed in their own sections

Output settings

KeyDefaultWhat it does
pathrequiredOutput folder
formatsvgsvg, png, jpg, webp, gif, pdf; with motion.enabled also mp4 and webm. One format per target
scale1Export scale in Figma, from 0.01 to 4
quality100Quality of raster formats, 1 to 100
losslessfalsetrue keeps WebP lossless, also when qualitySize is set: the CLI never switches to lossy encoding on its own
unsafeSvgPolicyfailWhat to do with an SVG icon that has an image fill inside a vector: fail stops the sync, embed-png writes an SVG with an embedded PNG. See SVG with embedded images
qualitySizenullSize limit for raster files, for example "50kb" or "1mb"; the quality is reduced until the file fits
width, heightnullResize raster output to these dimensions in pixels
modeWHnullHow to fit into width and height: null plain resize, proportion keep the aspect ratio inside the box, cover fill the box and crop, fill stretch

SVG files are optimised with SVGO, raster files with Sharp. webp and static gif are exported from Figma as PNG and converted locally; pdf comes from Figma as is.

Naming

A file name is built from naming.pattern. Tokens that are empty for a given icon are skipped; the rest are joined with separator and converted to case.

TokenValue
{prefix}, {suffix}naming.prefix and naming.suffix
{baseName}Component set name, otherwise the component name
{componentName}, {componentSetName}, {nodeName}, {nodeId}Figma names and the node id
{pageName}, {sourceId}Page name, source id
{sectionName}Section name, only when includeSectionName: true
{variant}All variant properties as property-value pairs
{variant.<prop>}One variant property, for example {variant.style} or {variant.size}. Chain several tokens for several properties; a property missing on the icon is skipped
KeyDefaultWhat it does
casekebabkebab, snake, camel, pascal
separatorby case- for kebab, _ for snake, none for camel and pascal
prefix, suffixnullValues for the {prefix} and {suffix} tokens
includeSectionNamefalseEnables the {sectionName} token
collisionStrategyadd-nodeidTwo icons get the same name: error stops, add-nodeid appends the node id, add-index appends a number

Without pattern the name is prefix, the style (or variant) property, the section when enabled, the set or component name, suffix.

Duplicates are checked before download. In an interactive terminal the CLI lists them and asks what to do: refresh re-reads Figma after you fix the names, skip keeps the first icon of each pair, all downloads everything and applies the collision strategy, abort stops. Choose with the arrow keys and Enter. In CI, without a terminal, the strategy from the config is applied without asking.

Overrides

Overrides change output, sourceExport, naming, motion, font or sfSymbols for the icons that match a rule, and can add selectors.

YAML
targets:
  - id: web-icons
    sourceIds: [mono]
    output:
      path: assets/icons
      format: svg
    overrides:
      - name: flags-webp
        match:
          sectionName: Flags
        patch:
          output:
            format: webp
            quality: 85

match accepts pageName, pageId, sectionName, sectionId, componentName, componentSetName, nodeId and variantMatch. Changing an override re-exports only the icons it matches.

Several formats, sources and configs

  • Several formats of the same icons: several targets with the same sourceIds and different output.path and format.
  • Several pages or sections of one file: several sources with the same fileKeyVar and different pageName or sectionName.
  • Several Figma files: several sources with different fileKeyVar; a target may list all of them in sourceIds.
  • Several packages: run sync with several --config flags or with --config-glob.

SVG with embedded images

Figma may return an incomplete SVG for a vector layer that has an image fill. Before writing files, the CLI checks the icons of every SVG target for this case.

  • unsafeSvgPolicy: fail (default): the sync stops and lists the icons. Fix them in Figma when the result must stay vector.
  • unsafeSvgPolicy: embed-png: such icons are downloaded from Figma as PNG at output.scale (up to 4) and wrapped into an SVG with the image embedded, so the file does not depend on a temporary Figma link. The content is raster and blurs when enlarged. Such SVGs cannot feed the icon font or SF Symbols, so keep them in a separate target without these pipelines, for example one selected with includeComponentNames.

--dry-run shows which icons would get an embedded PNG; the JSON report lists them under svgExport.embeddedPng.

Raster export through the plugin

By default raster formats are rendered by the Figma REST API at output.scale, at most 4. When a small vector must become a large raster, for example a 128 px WebP from a 24 px icon, let the SXL Studio plugin render the intermediate image instead: sourceExport.transport: plugin. sourceExport works at target level and inside overrides[].patch.

KeyDefaultWhat it does
transportrestplugin renders the intermediate image in the open Figma file through Bridge and Remote Connect
formatpngIntermediate format, png or jpg; output.format is applied afterwards
constraint.type, constraint.valuenoneSize of the intermediate: SCALE up to 16, WIDTH or HEIGHT up to 4096 px

Requirements of plugin transport:

  • output.format is png, jpg, webp or gif; it cannot be combined with motion.enabled.
  • Bridge is running and the SXL Studio plugin is open in the source file with Remote Connect on. One sync may use plugin transport for one Figma file only; split other files by config or --target.
  • The source has fileKey or fileKeyVar, also in mode: mcp. The CLI compares it with the open document before writing anything.
  • The intermediate must already contain enough pixels: the CLI does not enlarge locally. Raise constraint.value when you get an error about a source smaller than the requested output.
  • Limits per icon: sides up to 4096 px, 16 million pixels, 16 MiB of PNG or JPG.

Community plugin. The Community build of the plugin may not know the key of the open file. Open Repository connection in SXL Studio (the same field is in the Git Sync settings), fill in Figma file URL or key and save. The key is stored in the document for everyone; enter it again after duplicating a file. When the key is missing or does not match the source, the sync stops.

Example: a 24x24 vector from the Illustrations section is rendered by Figma at 128 px and saved as a lossless 128x128 WebP.

YAML
overrides:
  - name: illustrations-webp-128
    match:
      sectionName: Illustrations
    patch:
      output:
        format: webp
        width: 128
        height: 128
        quality: 100
        lossless: true
      sourceExport:
        transport: plugin
        format: png
        constraint:
          type: WIDTH
          value: 128

--dry-run prints how many icons will go through the plugin; the JSON report records each of them with its format and constraint.

Animated icons (Figma Motion)

Icons animated with Figma Motion are exported by the normal sync when the target sets motion.enabled: true.

FormatHow it is produced
svgAnimated SVG: the icon's SVG plus its Motion keyframes as CSS animation
gif, mp4, webmRendered by Figma itself through the plugin

Requirements: Bridge is running, the SXL Studio plugin is open in the same file with Remote Connect on, and the source is either rest with a file key or mcp. Motion targets are re-exported on every sync: the animation data comes from the plugin, not from the file snapshot that change tracking compares.

YAML
targets:
  - id: motion-webm
    sourceIds: [icons]
    output:
      path: assets/motion
      format: webm
    motion:
      enabled: true
      fps: 30
      loop: true
KeyDefaultWhat it does
enabledfalseTurns Motion export on for the target
fps30Frames per second, 1 to 120
looptrueLoop the animation
includeDescendantstrueInclude the animation of nested layers; descendantMaxDepth (8) and descendantMaxNodes (300) limit the traversal
ignoreOverlappingLayerstrueThe Figma export option of the same name; applies to svg
includeIdAttributetrueKeep layer ids as id attributes; applies to svg

To see what Figma reports for an animated icon before configuring a target, run probe-motion. Without --node-id it inspects the current selection in Figma.

BASH
pnpm exec sxl-export-icons probe-motion --format MP4,WEBM,GIF,SVG --inspect-only
pnpm exec sxl-export-icons probe-motion --node-id 1:2 --format SVG --include-id-attribute

Icon font and SF Symbols

Both pipelines run inside a target after the download and take the target's SVG files.

YAML
font:
  enabled: true
  path: assets/icons/font
  formats: [woff2, woff, ttf, eot, svg]
  fontName: icons
  engine: webfonts-generator
sfSymbols:
  enabled: true
  path: assets/icons/ios
KeyDefaultWhat it does
font.enabledfalseBuild the font
font.pathassets/fontOutput folder. It gets the font files, <fontName>.css, preview.html and codepoints.json, which keeps the code points stable between runs
font.formats[woff2, woff, ttf]Any of woff2, woff, ttf, eot, svg
font.fontNameiconsFont family and file name
font.enginewebfonts-generatorwebfonts-generator or advanced
font.includeNames, font.excludeNamesall iconsSubset by file name, with or without .svg
sfSymbols.enabledfalseBuild Assets.xcassets with a .symbolset per icon
sfSymbols.pathassets/iosOutput folder

SF Symbols skip icons with gradients, filters, masks or embedded images.

To build the font or SF Symbols from SVG files that are already on disk, use a convert-local target: sourceIds: [], download.enabled: false, output.format: svg and at least one of font or sfSymbols enabled. Run it alone with --target.

YAML
targets:
  - id: icons-font-only
    mode: convert-local
    sourceIds: []
    download:
      enabled: false
    output:
      path: assets/icons
      format: svg
    font:
      enabled: true
      path: assets/icons/font
      includeNames: [filled-menu, outline-menu]

State and change tracking

The CLI remembers every exported icon in state.file, per pair sourceId::targetId. On the next run it compares Figma with the state and acts only on the differences.

ChangeWhen
NEWThe icon appears for the first time
UPDATEDThe icon changed in Figma (shape edits included), the file is missing, the format changed, or the target settings changed
RENAMEDThe name changed, the format did not
DELETEDThe icon left the source scope; the file is deleted
UNCHANGEDNothing to do
  • First run without state: every icon is NEW and is downloaded, even when the files exist. --adopt-existing accepts the existing files as UNCHANGED instead.
  • --full clears the state and exports everything again.
  • download.pruneUntracked: true also deletes files of the target's format that the state does not know about.
  • report shows per pair how many icons are tracked, whether their files exist and in which formats.
  • clean deletes the tracked files and the font and SF Symbols folders, then clears the state; --skip-aux keeps those folders, --dry-run only lists.

If something does not work

What happenedWhat to do
Config file not foundPass --config <path> or create sxl-export-icons.config.yaml in the current folder
Missing Figma token in env "FIGMA_TOKEN"Add the token to .env or check env.figmaTokenVar
Figma answers 429Lower downloadSpeed of the source
Path ... is outside workspaceSet safety.allowOutsideWorkspace: true or run the command from the folder that contains the output
Bridge session is not connected, Remote Connect is not connectedStart Bridge, open SXL Studio in the file and turn on Remote Connect. See Bridge
Naming collision detectedAdd tokens to naming.pattern, for example {sectionName} or {nodeId}, or change collisionStrategy
Unsafe Figma SVG export blockedRemove the image fill in Figma, or set unsafeSvgPolicy: embed-png for these icons in a separate target
Raster-backed SVG fallback cannot be used as an icon-font or SF Symbols sourceMove the icons with embed-png to a target without font and sfSymbols
Plugin export is connected to Figma file "...", could not verify the active Figma fileOpen the source file in Figma; in the Community plugin fill in Figma file URL or key under Repository connection
Raster source ... is smaller than the requested outputIncrease sourceExport.constraint.value
Lossless WebP is ... above qualitySizeRaise qualitySize or set lossless: false
Wizard mode requires an interactive terminalRun init --wizard in a terminal; in CI use plain init
Every icon is NEW on the first run although the files existRun the first sync with --adopt-existing
Motion icons are exported on every runExpected: the animation data comes from the plugin, not from the file snapshot
No targets matched --target filterCheck the target id values; the message lists the available ones