Sync Figma design tokens into CSS, Tailwind, and TypeScript — without the copy-paste tax.
Tokensmith is a Python CLI for senior frontend teams who keep a Figma file as the source of truth for colors, spacing, radii, and typography. It pulls local variables (including light/dark modes) from the Figma Variables API, normalizes them into a typed token model, and exports idiomatic code artifacts you can commit and review.
Figma variables ──pull──► .tokensmith/snapshot.json ──export──► tokens.css / tokens.ts / tailwind.tokens.js
│
└──diff──► Rich table of added / removed / modified tokens
Design systems drift. A designer updates --color-primary in Figma; engineering still ships last quarter’s hex in globals.css. Manual sync is slow, error-prone, and invisible in code review.
Tokensmith makes the pipeline explicit:
- Pull variables from a real Figma file (PAT + file key).
- Export to CSS custom properties, a Tailwind config partial, or a typed
tokens.ts. - Diff the latest pull against the last export so CI (or you) can fail when tokens drift.
Your Figma personal access token is never written to disk, committed, or logged. Pass --token or set FIGMA_TOKEN.
Requires Python 3.11+.
With uv (recommended)
# from this repo
uv sync
uv run tokensmith --help
# or install as a tool
uv tool install .pipx install .
tokensmith --helppip install -e ".[dev]"Create a tokensmith.toml in your project (see tokensmith.toml.example):
file_key = "YOUR_FIGMA_FILE_KEY"
default_format = "css"
output_path = "tokens"
default_mode = "Light"
[mode_selectors]
Dark = ".dark, [data-theme='dark']"Export a Figma personal access token with the file_variables:read scope, then:
export FIGMA_TOKEN="figd_••••" # never commit this
tokensmith pull --file-key YOUR_FIGMA_FILE_KEY
# Pulled 12 token(s) across 2 mode(s) → .tokensmith/snapshot.json
# Modes: Dark, Light
tokensmith export --format css
# Wrote tokens/tokens.css
# Exported 12 token(s) as css.
tokensmith export --format ts
# Wrote tokens/tokens.ts
tokensmith export --format tailwind
# Wrote tokens/tailwind.tokens.jsExample CSS output (modes become separate scopes):
/* Generated by tokensmith — do not edit by hand. */
:root {
--color-accent: #2563eb;
--color-primary: #2563eb;
--color-surface: #ffffff;
--radius-lg: 12px;
--spacing-md: 16px;
--typography-body: Inter;
}
[data-theme="dark"] {
--color-accent: #60a5fa;
--color-primary: #60a5fa;
--color-surface: #12171c;
--radius-lg: 12px;
--spacing-md: 16px;
--typography-body: Inter;
}After a designer changes tokens in Figma:
tokensmith pull
tokensmith diff
# 1 change(s) (+0 / -0 / ~1)
#
# ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
# ┃ Status ┃ Token ┃ Mode ┃ Before ┃ After ┃
# ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
# │ modified │ Color/Primary │ Light │ color:#2563eb │ color:#111111 │
# └────────────┴───────────────┴───────┴───────────────┴───────────────┘tokensmith diff exits 1 when changes exist (CI-friendly) and 0 when the export is in sync.
| Command | Purpose |
|---|---|
tokensmith pull |
Fetch local variables → .tokensmith/snapshot.json |
tokensmith export |
Write css / tailwind / ts from the snapshot |
tokensmith diff |
Compare latest pull vs last export (Rich table) |
tokensmith version |
Print package version |
Flags of note:
--file-key/file_keyin config--tokenorFIGMA_TOKEN(never stored)--format css|tailwind|ts,--allfor every format in config--outputto overrideoutput_path
Figma variable modes are first-class:
- CSS — default mode →
:root; other modes →[data-theme="…"](override with[mode_selectors]). - TypeScript — one
as constexport per mode (tokensLight,tokensDark, …) plustokensByMode. - Tailwind — default mode under
theme.extend; other modes under amodesmap for your own theme plugin.
tokensmith.toml (repo root):
| Key | Description |
|---|---|
file_key |
Default Figma file key |
default_format |
css, tailwind, or ts |
output_path |
Directory for generated files (default tokens) |
formats |
Formats used by export --all |
default_mode |
Mode mapped to :root / primary export |
mode_selectors |
Custom CSS selectors per mode name |
aliases |
Rename Figma variable names on export |
Local state (gitignored by default):
.tokensmith/snapshot.json— last pull.tokensmith/export_manifest.json— last export (diff baseline)
Tokensmith is built so a scheduled job can fail (or open a PR) when Figma moves and the repo does not.
Example GitHub Actions sketch:
name: Sync design tokens
on:
schedule:
- cron: "0 8 * * 1-5" # weekdays 08:00 UTC
workflow_dispatch:
jobs:
tokens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync
- name: Pull & export
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
run: |
uv run tokensmith pull
uv run tokensmith export --all
- name: Detect drift
id: drift
run: |
if uv run tokensmith diff; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Open PR when tokens change
if: steps.drift.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore(tokens): sync from Figma"
title: "chore(tokens): sync from Figma"
body: |
Automated token sync via tokensmith.
Review the CSS / Tailwind / TS diffs carefully.
branch: chore/tokensmith-syncStore the Figma PAT as FIGMA_TOKEN in repository secrets. Never echo it in logs.
This repo’s own CI (.github/workflows/ci.yml) runs ruff and pytest on push/PR — all Figma calls are mocked; no live API access is required.
uv sync --extra dev
uv run ruff check src tests
uv run pytestTests use httpx.MockTransport and fixtures under tests/fixtures/ so CI never hits api.figma.com.
To exercise a live Figma file:
export FIGMA_TOKEN="figd_…"
uv run tokensmith pull --file-key YOUR_FILE_KEYNot implemented yet — candidates for a follow-up:
tokensmith watch— poll Figma on an interval and auto re-export- First-class GitHub API integration to open a PR when tokens change (today: compose with
create-pull-requestas above) - Support for Figma styles (not just variables) for older files
MIT — see LICENSE.