Utilities

Export Icons

SXL Export Icons: installable CLI utility to export COMPONENT/COMPONENT_SET assets from Figma with diff sync, multi-config, fonts, and SF Symbols.

What Export Icons is

SXL Export Icons (@sxl-studio/export-icons) is a CLI utility for large-scale icon and asset export from Figma into your project.

It:

  • exports only COMPONENT and COMPONENT_SET variants;
  • supports svg, png, jpg, webp, gif, pdf;
  • runs incremental sync (new/updated/deleted/renamed);
  • generates icon fonts (woff2/woff/ttf/svg/eot);
  • generates iOS Assets.xcassets symbol sets;
  • supports many configs and many source/target pairs in one run;
  • supports both Figma REST and Bridge/MCP source modes.

Installation

BASH
npm install --save-dev @sxl-studio/export-icons

# run locally installed binary
pnpm exec sxl-export-icons help

# one-shot without installing
pnpm dlx @sxl-studio/export-icons help
npx @sxl-studio/export-icons help

Avoid pnpx sxl-export-icons ... in proxy/private registries.
It may resolve the unscoped package sxl-export-icons and fail with 404.


Preparing Figma access

1) Personal Access Token

  1. Open Figma account settings.
  2. Create a Personal Access Token.
  3. Enable at least these scopes:
    • file_content:read
    • file_metadata:read
  4. No write scopes are required for export-icons.
  5. Set token expiration according to your policy (Figma PATs are time-limited).
  6. Save it in .env:
BASH
FIGMA_TOKEN=figd_xxxxxxxxxxxxxxxxx

2) fileKey

From file URL:

https://www.figma.com/design/<fileKey>/...

Save in .env:

BASH
SXL_ICONS_FILE_KEY=abc123xyz456

3) Secure your .env

Add .env to .gitignore and never commit access tokens:

GITIGNORE
.env
.env.local

Minimal env set:

BASH
FIGMA_TOKEN=figd_xxxxxxxxxxxxxxxxx
SXL_ICONS_FILE_KEY=xxxxxxxxxxxx
# optional, only for mode: mcp
BRIDGE_AUTH_TOKEN=xxxxxxxxxxxx

CLI auto-loads .env.local and .env from current directory and parent directories. REST loading now shows live progress stages/pages (REST 1/3, REST 2/3, REST 3/3).


Quick start

BASH
# 1. Generate starter config
pnpm exec sxl-export-icons init

# or interactive wizard
pnpm exec sxl-export-icons init --wizard

# 2. Validate
pnpm exec sxl-export-icons validate-config --config sxl-export-icons.config.yaml

# 3. Dry-run
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --dry-run

# 4. First real run (adopt already-downloaded files as baseline)
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --adopt-existing

# 5. Regular sync
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml

# 6. Run only one target from mixed config
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --target font-subset-local

Project terminal example

BASH
cd ./design-system/tokens
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

If you run from a token package, add scripts like these to its package.json:

BASH
pnpm run icons:sync:init        # first run: baseline from existing files
pnpm run icons:sync             # day-to-day Figma sync
pnpm run icons:sync:dry-run     # preview changes
pnpm run icons:sync:full        # emergency full rebuild
pnpm run icons:sync:font        # local subset icon -> font conversion
pnpm run icons:sync:font:dry-run
pnpm run icons:clean

CLI commands

CommandPurpose
sync (default)Export/sync icons
cleanRemove generated files tracked by state for selected scopes
initGenerate starter sxl-export-icons.config.yaml
validate-configValidate YAML and path/env resolution
reportPrint state coverage per source/target scope

Command aliases:

  • create-config -> init
  • init-wizard -> init --wizard

Useful flags:

  • --config <path> (repeatable)
  • --config-glob "<glob>"
  • --target <target-id> (repeatable)
  • --dry-run
  • --full
  • --adopt-existing
  • --allow-fallback-rest
  • --report <path-to-json>

clean supports additional options:

  • --skip-aux -> keep generated font.path and sfSymbols.path folders while cleaning tracked asset files/state
  • --dry-run -> preview deleted files without filesystem changes

Config v3 structure

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: []
targets: []

state.file note:

  • schema default is assets/.cache/sxl-export-icons-state.json;
  • init template/wizard suggests .sxl/cache/icons-state.json.

Both values are valid. Use one stable path per repository.

Why bridge exists in config

bridge belongs to Utils/bridge and is used only when sources[].mode: mcp (Remote Connect / Bridge API).
If you use only mode: rest, the bridge block is ignored.

Interactive setup wizard (init --wizard)

Wizard builds a valid baseline config via prompts:

  • language (en/ru, default en);
  • whether to create .env;
  • state.file location;
  • safety.allowOutsideWorkspace;
  • base source and target settings;
  • font pipeline / SF Symbols toggles;
  • optional description marker filter.

Command:

BASH
pnpm exec sxl-export-icons init --wizard

Legacy version: 2 config (icons.config.yaml) is also supported via auto-migration.
For version: 2, relative paths are resolved from workspace root to preserve old script behavior.

sources[]

YAML
sources:
  - id: mono
    mode: rest # rest | mcp
    fileKeyVar: SXL_ICONS_FILE_KEY
    pageName: Monochrome
    sectionName: Actions
    downloadSpeed: 20
    selectors:
      includeNodeIds: []
      excludeNodeIds: []
      includeComponentSetNames: []
      excludeComponentSetNames: []
      includeComponentNames: []
      excludeComponentNames: []
      variantMatch:
        styleMode: outlined

Key fields:

  • mode: rest -> direct Figma REST.
  • mode: mcp -> SXL Bridge Remote Connect traversal.
  • fileKey / fileKeyVar -> required for REST.
  • pageName|pageId, sectionName|sectionId -> source narrowing.
  • selectors.variantMatch -> any variant prop key (not only style).
  • With sectionName|sectionId, only nodes inside that section are exported.

selectors reference:

  • includeNodeIds / excludeNodeIds -> allow/deny specific node IDs
  • includeComponentNames / excludeComponentNames -> allow/deny component names
  • includeComponentSetNames / excludeComponentSetNames -> allow/deny component set names
  • variantMatch -> exact match by variant props; value can be either string or array

Example with multi-value variantMatch:

YAML
selectors:
  variantMatch:
    state: [default, hover]
    styleMode: outlined

Description marker filter

You can gate export by description of COMPONENT / COMPONENT_SET:

YAML
sources:
  - id: mono
    mode: rest
    fileKeyVar: SXL_ICONS_FILE_KEY
    descriptionExportMarker:
      key: sxl-studio-export-icon
      includeWhenMissing: true

Behavior:

  • sxl-studio-export-icon: true -> included;
  • sxl-studio-export-icon: false -> excluded;
  • marker missing -> follows includeWhenMissing.

targets[]

YAML
targets:
  - id: web-icons
    mode: sync # sync | convert-local
    sourceIds: [mono]
    selectors:
      excludeComponentNames: [deprecated-icon]
    download:
      enabled: true
      pruneUntracked: false
    output:
      path: assets/icons
      format: svg
      scale: 1
      quality: 100
      qualitySize: null
      width: null
      height: null
      modeWH: null
    naming:
      case: kebab
      separator: "-"
      pattern: "{prefix}{variant.styleMode}{sectionName}{baseName}{suffix}"
      prefix: null
      suffix: null
      includeSectionName: false
      collisionStrategy: add-nodeid

Target-level filtering and override-level filtering:

  • targets[].selectors filters indexed source items before output/naming are resolved.
  • targets[].overrides[].patch.selectors applies an additional selector gate for that override rule only.

One or multiple formats

target.output.format is one format per target.

If you need multiple formats for the same icons, add multiple targets that share the same sourceIds:

YAML
targets:
  - id: mono-svg
    sourceIds: [mono]
    output:
      path: assets/icons/svg
      format: svg
      scale: 1
      quality: 100
      qualitySize: null
      width: null
      height: null
      modeWH: null
  - id: mono-webp
    sourceIds: [mono]
    output:
      path: assets/icons/webp
      format: webp
      scale: 1
      quality: 90
      qualitySize: null
      width: null
      height: null
      modeWH: null

Naming and pattern tokens

Pattern tokens:

  • {prefix}, {suffix}
  • {sectionName}, {pageName}
  • {baseName}, {componentName}, {componentSetName}, {nodeName}, {nodeId}
  • {variant} (all variant props)
  • {variant.<propName>} (specific variant prop)

variant rules:

  • variant is a reserved token namespace (do not rename it).
  • <propName> is any Figma variant property name (for example style, type, size, state).
  • For multi-prop naming, chain tokens in order:
    • "{prefix}{pageName}{variant.style}{variant.type}{baseName}"
  • If a node does not have that prop, the segment is skipped. In mixed sets this can produce collisions, so use collisionStrategy and/or include extra tokens ({sectionName}, {nodeId}).

Example:

YAML
pattern: "{prefix}{variant.styleMode}{baseName}{suffix}"

Collision strategy:

  • error
  • add-nodeid
  • add-index

Duplicate preflight (interactive)

Before download, if duplicate output names are detected, CLI prints duplicate entries and asks:

  • refresh — reload source and re-check duplicates
  • skip — keep one and skip duplicate nodes
  • all — download all and resolve names via collision strategy
  • abort — stop sync

In interactive terminal mode, action selection uses keyboard navigation (/ + Enter). In non-interactive sessions (CI), prompts are skipped and configured collisionStrategy is used.


Overrides

Use overrides to patch output/naming/font/SF options per match rule:

YAML
overrides:
  - name: flags-webp
    match:
      sectionName: Flags
    patch:
      output:
        format: webp
        quality: 85
  - name: outlined-only
    match:
      variantMatch:
        styleMode: outlined
    patch:
      naming:
        suffix: outlined
      selectors:
        excludeComponentNames: [legacy-icon]
  - name: ios-symbolset-only
    match:
      pageName: iOS
    patch:
      sfSymbols:
        enabled: true
      font:
        enabled: false

match fields support:

  • pageName, pageId
  • sectionName, sectionId
  • componentName, componentSetName
  • nodeId
  • variantMatch

Multi-config and multi-source

Multiple config files

BASH
npx sxl-export-icons sync \
  --config packages/ds-a/sxl-export-icons.config.yaml \
  --config packages/ds-b/sxl-export-icons.config.yaml

Multiple sources in one config

YAML
sources:
  - id: mono
    mode: rest
    fileKeyVar: FILE_A
  - id: flags
    mode: rest
    fileKeyVar: FILE_B

targets:
  - id: web
    sourceIds: [mono, flags]
    output:
      path: assets/icons
      format: svg
      scale: 1
      quality: 100
      qualitySize: null
      width: null
      height: null
      modeWH: null
    naming:
      case: kebab
      collisionStrategy: add-nodeid

For multiple pages/sections from the same Figma file, create multiple source entries with the same fileKeyVar and different pageName/sectionName.


Exporting outside workspace

Blocked by default.

To allow explicitly:

YAML
safety:
  allowOutsideWorkspace: true

allowOutsideWorkspace: false (default) protects against accidental writes outside repository root.
Set true only when you intentionally need external/absolute output paths.

This enables paths outside current workspace root.


Font and SF Symbols pipelines

Icon font

YAML
font:
  enabled: true
  path: assets/icons/font
  formats: [woff2, woff, ttf, eot, svg]
  fontName: icons
  engine: webfonts-generator # webfonts-generator | advanced
  includeNames: [] # optional subset by canonical icon names
  excludeNames: [] # optional exclusions

SF Symbols assets

YAML
sfSymbols:
  enabled: true
  path: assets/icons/ios

Limitation: complex SVG (gradients, filters, masks, embedded images) is skipped as incompatible.

Convert-only target (local SVG -> font/SF)

If SVG files already exist in output path and you need only conversion:

YAML
targets:
  - id: icons-font-only
    mode: convert-local
    sourceIds: []
    download:
      enabled: false
    output:
      path: assets/icons
      format: svg
      scale: 1
      quality: 100
      qualitySize: null
      width: null
      height: null
      modeWH: null
    naming:
      case: kebab
      separator: "-"
      pattern: "{variant.style}{baseName}"
      includeSectionName: false
      collisionStrategy: add-nodeid
    font:
      enabled: true
      path: assets/icons/font
      formats: [woff2, woff, ttf, eot]
      fontName: icons
      includeNames: [filled-casino-menu, outline-casino-menu]

download.enabled: false skips download/rename/delete and uses existing local SVG files.

font.includeNames / font.excludeNames accept names with or without .svg.


Clean and report workflows

Clean tracked output for selected config scopes:

BASH
pnpm exec sxl-export-icons clean --config ./sxl-export-icons.config.yaml

Keep font/SF folders while cleaning tracked icon files:

BASH
pnpm exec sxl-export-icons clean --config ./sxl-export-icons.config.yaml --skip-aux

Preview clean result:

BASH
pnpm exec sxl-export-icons clean --config ./sxl-export-icons.config.yaml --dry-run

Generate state coverage report (JSON):

BASH
pnpm exec sxl-export-icons report --config ./sxl-export-icons.config.yaml
pnpm exec sxl-export-icons sync --config ./sxl-export-icons.config.yaml --report ./.reports/icons-report.json

Diff and state

State is saved in state.file and tracked per scope sourceId::targetId.

Change classes:

  • NEW
  • UPDATED
  • DELETED
  • RENAMED
  • UNCHANGED

UPDATED also includes format changes (svg -> webp) and scope config hash changes (source + target) to force proper re-export. For REST mode, per-node fingerprint is built from render-relevant node data (geometry=paths), so pure vector shape edits are detected and re-exported.

First run without state treats all matched icons as NEW (re-export), even if files already exist on disk.

Use --adopt-existing on first real run if you want to adopt existing files as baseline UNCHANGED.

Optional strict cleanup:

  • set download.pruneUntracked: true to remove untracked files with the same output format in output.path;
  • use it only for dedicated directories (cleanup is path+format based).

Source modes: REST vs MCP

REST

  • Stable and predictable.
  • No active plugin Remote Connect session required.

MCP (Bridge)

  • Reads through active SXL plugin session context.
  • Useful in orchestrated agent workflows.

Requirements:

  • Utils/bridge running;
  • Remote Connect enabled in SXL Studio plugin;
  • valid bridge.baseUrl.

With --allow-fallback-rest, failed MCP source auto-falls back to REST if fileKey is available.

Target modes: sync vs convert-local

  • mode: sync (default): reads Figma source, syncs files, then runs post-processing.
  • mode: convert-local: skips Figma source and uses existing local SVG files from output.path for font/SF generation.

convert-local requirements:

  • sourceIds: []
  • download.enabled: false
  • output.format: svg
  • enable font or sfSymbols

Common errors

  • Missing Figma token
    Check env var from env.figmaTokenVar.

  • outside workspace
    Set safety.allowOutsideWorkspace: true if intentional.

  • Bridge session is not connected
    Start Bridge and enable plugin Remote Connect.

  • Naming collision detected
    Update naming.pattern or set collisionStrategy: add-nodeid/add-index.


Best practices

  • Keep secrets (FIGMA_TOKEN, file keys) in .env only.
  • Use --dry-run before large sync runs.
  • Split large libraries into multiple sources by page.
  • Keep naming stable; treat pattern changes as migration events.
  • Save --report JSON in CI artifacts.