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 get | Details |
|---|---|
| Static files | svg, png, jpg, webp, gif, pdf |
| Animated icons (Figma Motion) | svg, gif, mp4, webm |
| Icon font | woff2, woff, ttf, eot, svg with a stylesheet and a preview page |
| iOS | Assets.xcassets with a .symbolset per icon |
| Change tracking | New, updated, renamed and deleted icons are detected between runs |
Quick start
- Install the package into the project. Node.js 20.9 or newer is required.
- Put the Figma token and the file key into
.env(see Access to Figma below). - Create a config with the wizard: it asks a few questions and writes a valid file.
- Check the config and preview the changes with
--dry-run. - Run the sync. If the folder already contains icons downloaded earlier, add
--adopt-existingto the first run: the files are accepted as the baseline instead of being downloaded again.
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
- Open Figma account settings and create a personal access token with the scopes
file_content:readandfile_metadata:read. Write scopes are not needed. Figma tokens expire, so choose a lifetime that suits your policy. - Take the file key from the file URL:
https://www.figma.com/design/<fileKey>/.... - Save both in
.envat the repository root, and add.envand.env.localto.gitignore.
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
| Command | What it does |
|---|---|
sync (default) | Reads the sources, downloads new and changed icons, removes deleted ones, then builds the font and SF Symbols |
init | Writes the starter sxl-export-icons.config.yaml; with --wizard asks questions instead of copying the template |
validate-config | Checks the YAML, the paths and the environment variables |
report | Prints, per source and target, how many icons are tracked and whether their files exist |
clean | Deletes the files listed in the state and clears the state |
probe-motion | Shows what Figma reports for a Motion node, without writing files |
help, version | Help 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
| Flag | What 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-run | Shows what would change and writes nothing |
--full | Clears 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-existing | On the first run accepts the files already on disk as unchanged instead of downloading them again |
--allow-fallback-rest | If 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
| Command | Flag | What it does |
|---|---|---|
init | --path <path> | Where to write the config; default sxl-export-icons.config.yaml |
init | --force | Overwrite an existing config |
init | --wizard | Interactive questions; needs a real terminal |
clean | --dry-run | Only list what would be deleted |
clean | --skip-aux | Keep the font and SF Symbols folders; delete only the tracked icon files |
validate-config, report | --config, --config-glob | Same 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-descendants | Read 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.
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
| Key | Default | What it does |
|---|---|---|
env.figmaTokenVar | FIGMA_TOKEN | Name of the variable with the Figma token |
bridge.baseUrl | http://127.0.0.1:37830 | Address of Bridge. Used only by mode: mcp sources, Motion targets and plugin transport |
bridge.authTokenVar | BRIDGE_AUTH_TOKEN | Variable with the Bridge access token |
state.file | assets/.cache/sxl-export-icons-state.json | Where 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.allowOutsideWorkspace | false | By 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
sources:
- id: mono
mode: rest
fileKeyVar: SXL_ICONS_FILE_KEY
pageName: Monochrome
sectionName: Actions
downloadSpeed: 20
descriptionExportMarker:
key: sxl-studio-export-icon
includeWhenMissing: true
| Key | Default | What it does |
|---|---|---|
id | required | Name used in targets[].sourceIds |
mode | rest | rest 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, fileKeyVar | required for rest | The file key itself or the name of the variable that holds it |
pageName, pageId | all pages | Read only this page |
sectionName, sectionId | all sections | Read only the icons inside this section |
downloadSpeed | 10 | Requests per second to Figma. Lower it when Figma answers 429 |
descriptionExportMarker | off | Filter by a marker in the component description, see below |
selectors | none | Include and exclude rules, see below |
| Selector | What it does |
|---|---|
includeNodeIds, excludeNodeIds | By node id |
includeComponentNames, excludeComponentNames | By component name |
includeComponentSetNames, excludeComponentSetNames | By component set name |
variantMatch | Exact match of variant properties. Any property works, not only style; the value is a string or a list of strings |
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
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
| Key | Default | What it does |
|---|---|---|
id | required | Target name, used with --target |
mode | sync | sync reads Figma and writes files. convert-local takes the SVG files already in output.path and only builds the font or SF Symbols |
sourceIds | required for sync | Sources to export |
selectors | none | Additional filter for this target |
download.enabled | true | false disables download, rename and delete; the local files are used as they are |
download.pruneUntracked | false | Also 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 |
output | Format and processing, see the next table | |
naming | File names, see Naming | |
overrides, sourceExport, motion, font, sfSymbols | off | Described in their own sections |
Output settings
| Key | Default | What it does |
|---|---|---|
path | required | Output folder |
format | svg | svg, png, jpg, webp, gif, pdf; with motion.enabled also mp4 and webm. One format per target |
scale | 1 | Export scale in Figma, from 0.01 to 4 |
quality | 100 | Quality of raster formats, 1 to 100 |
lossless | false | true keeps WebP lossless, also when qualitySize is set: the CLI never switches to lossy encoding on its own |
unsafeSvgPolicy | fail | What 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 |
qualitySize | null | Size limit for raster files, for example "50kb" or "1mb"; the quality is reduced until the file fits |
width, height | null | Resize raster output to these dimensions in pixels |
modeWH | null | How 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.
| Token | Value |
|---|---|
{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 |
| Key | Default | What it does |
|---|---|---|
case | kebab | kebab, snake, camel, pascal |
separator | by case | - for kebab, _ for snake, none for camel and pascal |
prefix, suffix | null | Values for the {prefix} and {suffix} tokens |
includeSectionName | false | Enables the {sectionName} token |
collisionStrategy | add-nodeid | Two 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.
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
sourceIdsand differentoutput.pathandformat. - Several pages or sections of one file: several sources with the same
fileKeyVarand differentpageNameorsectionName. - Several Figma files: several sources with different
fileKeyVar; a target may list all of them insourceIds. - Several packages: run
syncwith several--configflags 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 atoutput.scale(up to4) 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 withincludeComponentNames.
--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.
| Key | Default | What it does |
|---|---|---|
transport | rest | plugin renders the intermediate image in the open Figma file through Bridge and Remote Connect |
format | png | Intermediate format, png or jpg; output.format is applied afterwards |
constraint.type, constraint.value | none | Size of the intermediate: SCALE up to 16, WIDTH or HEIGHT up to 4096 px |
Requirements of plugin transport:
output.formatispng,jpg,webporgif; it cannot be combined withmotion.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
fileKeyorfileKeyVar, also inmode: 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.valuewhen 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.
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.
| Format | How it is produced |
|---|---|
svg | Animated SVG: the icon's SVG plus its Motion keyframes as CSS animation |
gif, mp4, webm | Rendered 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.
targets:
- id: motion-webm
sourceIds: [icons]
output:
path: assets/motion
format: webm
motion:
enabled: true
fps: 30
loop: true
| Key | Default | What it does |
|---|---|---|
enabled | false | Turns Motion export on for the target |
fps | 30 | Frames per second, 1 to 120 |
loop | true | Loop the animation |
includeDescendants | true | Include the animation of nested layers; descendantMaxDepth (8) and descendantMaxNodes (300) limit the traversal |
ignoreOverlappingLayers | true | The Figma export option of the same name; applies to svg |
includeIdAttribute | true | Keep 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.
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.
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
| Key | Default | What it does |
|---|---|---|
font.enabled | false | Build the font |
font.path | assets/font | Output 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.fontName | icons | Font family and file name |
font.engine | webfonts-generator | webfonts-generator or advanced |
font.includeNames, font.excludeNames | all icons | Subset by file name, with or without .svg |
sfSymbols.enabled | false | Build Assets.xcassets with a .symbolset per icon |
sfSymbols.path | assets/ios | Output 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.
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.
| Change | When |
|---|---|
NEW | The icon appears for the first time |
UPDATED | The icon changed in Figma (shape edits included), the file is missing, the format changed, or the target settings changed |
RENAMED | The name changed, the format did not |
DELETED | The icon left the source scope; the file is deleted |
UNCHANGED | Nothing to do |
- First run without state: every icon is
NEWand is downloaded, even when the files exist.--adopt-existingaccepts the existing files asUNCHANGEDinstead. --fullclears the state and exports everything again.download.pruneUntracked: truealso deletes files of the target's format that the state does not know about.reportshows per pair how many icons are tracked, whether their files exist and in which formats.cleandeletes the tracked files and the font and SF Symbols folders, then clears the state;--skip-auxkeeps those folders,--dry-runonly lists.
If something does not work
| What happened | What to do |
|---|---|
Config file not found | Pass --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 429 | Lower downloadSpeed of the source |
Path ... is outside workspace | Set safety.allowOutsideWorkspace: true or run the command from the folder that contains the output |
Bridge session is not connected, Remote Connect is not connected | Start Bridge, open SXL Studio in the file and turn on Remote Connect. See Bridge |
Naming collision detected | Add tokens to naming.pattern, for example {sectionName} or {nodeId}, or change collisionStrategy |
Unsafe Figma SVG export blocked | Remove 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 source | Move 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 file | Open 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 output | Increase sourceExport.constraint.value |
Lossless WebP is ... above qualitySize | Raise qualitySize or set lossless: false |
Wizard mode requires an interactive terminal | Run init --wizard in a terminal; in CI use plain init |
Every icon is NEW on the first run although the files exist | Run the first sync with --adopt-existing |
| Motion icons are exported on every run | Expected: the animation data comes from the plugin, not from the file snapshot |
No targets matched --target filter | Check the target id values; the message lists the available ones |