Bidirectional drift detection and sync between React components and Figma design systems.
Detects when your code and Figma components diverge, syncs changes in either direction, and auto-generates Storybook stories with interaction tests.
Most Figma tools are one-way — they generate code from designs. figma-drift keeps both sides in sync:
- Extracts component structure from React code — variants, props, design tokens, spacing (via ts-morph AST parsing)
- Extracts component structure from Figma — variants, color bindings, auto-layout, corner radius (via Figma MCP server)
- Detects drift — categorized differences: color, spacing, radius, variant, prop
- Syncs in either direction — push code changes to Figma canvas, or pull Figma changes into code
- Auto-generates Storybook stories — with interaction tests on every sync
- Manages design token parity — between CSS custom properties (OKLCH) and Figma variables (RGB)
figma-drift communicates with Figma through the Figma MCP server. You need it connected to your IDE/MCP client.
Quick setup — add to your MCP config (VS Code, Cursor, Kiro, etc.):
{
"mcpServers": {
"figma": {
"url": "https://mcp.figma.com/mcp"
}
}
}Rate limits:
- Starter / View / Collab seats: 6 tool calls per month
- Dev or Full seats on Professional, Organization, or Enterprise plans: per-minute limits (Tier 1 REST API)
Write to canvas (use_figma) is free during the beta period and will eventually become a usage-based paid feature. A Full seat is required for write operations; Dev seats are read-only.
- Node.js >= 18
- React + TypeScript project
- Components using class-variance-authority (cva) for variants — standard with shadcn/ui
- Tailwind CSS for styling and design token extraction
- Design tokens as CSS custom properties in your stylesheet (OKLCH or hex)
- Storybook (latest version recommended) — for auto-generated story files with interaction tests
npm install @effinrich/figma-driftfigma-drift ships as an MCP server that any MCP client can use. Add to your mcp.json:
{
"mcpServers": {
"figma-drift": {
"command": "npx",
"args": ["@effinrich/figma-drift-mcp"]
}
}
}This exposes 8 tools to your AI agent:
| Tool | Description |
|---|---|
figma_drift_init |
Initialize component map (scan code, match to Figma) |
figma_drift_detect |
Detect drift between code and Figma |
figma_drift_push |
Push code changes to Figma |
figma_drift_pull |
Pull Figma changes into code |
figma_drift_stories |
Generate Storybook stories with interaction tests |
figma_drift_manifest |
Extract component manifest from React source (no Figma needed) |
figma_drift_sync_all |
Full pipeline: drift → sync → stories |
figma_drift_tokens |
Sync design tokens between CSS and Figma variables |
The MCP server requires the Figma MCP server to be configured alongside it for Figma operations. Tools that don't need Figma (manifest extraction, story generation) work standalone.
Create a .figma-drift.json in your project root. Only fileKey is required — everything else has sensible defaults:
{
"fileKey": "YOUR_FILE_KEY"
}{
"fileKey": "YOUR_FILE_KEY",
"componentMapPath": ".kiro/sync/component-map.json",
"syncLogPath": ".kiro/sync/sync.log",
"componentDirs": ["src/components/ui", "src/components/dashboard"],
"storyDirMap": {
"components/ui/": "src/stories/atoms",
"components/dashboard/": "src/stories/molecules"
},
"defaultStoryDir": "src/stories",
"autoSync": false,
"preferDirection": "code-to-figma",
"autoStories": true
}| Option | Type | Default | Description |
|---|---|---|---|
fileKey |
string |
— | Required. Figma file key or full URL (key is extracted automatically) |
componentMapPath |
string |
.kiro/sync/component-map.json |
Path to the component map JSON file |
syncLogPath |
string |
.kiro/sync/sync.log |
Path to the sync log file |
componentDirs |
string[] |
["src/components/ui", "src/components/dashboard"] |
Directories to scan for React components |
storyDirMap |
Record<string, string> |
{"components/ui/": "src/stories/atoms", ...} |
Maps component path fragments to story output directories |
defaultStoryDir |
string |
src/stories |
Fallback story directory when no mapping matches |
autoSync |
boolean |
false |
When true, sync runs automatically without prompting |
preferDirection |
"code-to-figma" | "figma-to-code" |
— | Preferred sync direction when autoSync is true |
autoStories |
boolean |
true |
Auto-generate stories after every sync |
Configuration is resolved in priority order:
.figma-drift.jsonin project root"figmaDrift"field inpackage.jsonFIGMA_DRIFT_FILE_KEYenvironment variable
# Environment variable
export FIGMA_DRIFT_FILE_KEY="YOUR_FILE_KEY"// package.json
{
"figmaDrift": {
"fileKey": "YOUR_FILE_KEY",
"componentDirs": ["src/ui", "src/features"]
}
}npx @effinrich/figma-drift initThis scans the directories in componentDirs, matches components to Figma via search_design_system, and saves the map to componentMapPath.
# Initialize component map (scans code, matches to Figma)
npx @effinrich/figma-drift init
# Detect drift between code and Figma
npx @effinrich/figma-drift drift
npx @effinrich/figma-drift drift --json
# Push code changes to Figma
npx @effinrich/figma-drift push # all drifted components
npx @effinrich/figma-drift push Button # specific component
# Pull Figma changes into code
npx @effinrich/figma-drift pull # all drifted components
npx @effinrich/figma-drift pull Card # specific component
# Generate Storybook stories
npx @effinrich/figma-drift stories # all components
npx @effinrich/figma-drift stories Button # specific component
# Full pipeline: drift → sync → stories
npx @effinrich/figma-drift all --direction code-to-figma
npx @effinrich/figma-drift all --direction figma-to-code
npx @effinrich/figma-drift all --dry-run # preview without changesimport {
runDriftDetection,
runCodeToFigmaSync,
runFigmaToCodeSync,
runFullPipeline,
createFigmaMCPAdapter,
formatDriftReport,
} from "@effinrich/figma-drift";
import type { MCPToolCaller } from "@effinrich/figma-drift";
// Create an MCP tool caller (this is the bridge to your MCP client)
const myMCPCaller: MCPToolCaller = async (toolName, args) => {
// Your MCP client integration here
// Returns the raw string response from the Figma MCP tool
};
// Create adapter
const adapter = createFigmaMCPAdapter(myMCPCaller, {
fileKey: "YOUR_FILE_KEY",
});
// Detect drift
const report = await runDriftDetection(adapter);
console.log(formatDriftReport(report));
// → Components: 14 total, 11 in-sync, 2 drifted, 1 unlinked
// Sync a specific component to Figma
const result = await runCodeToFigmaSync(adapter, "Button");
console.log(result);
// → { success: true, componentName: "Button", direction: "code-to-figma", changesApplied: 3 }
// Pull Figma changes into code
const pullResult = await runFigmaToCodeSync(adapter, "Card");
// Full pipeline
const pipelineResult = await runFullPipeline(adapter, {
direction: "code-to-figma",
dryRun: false,
});┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ React Code │────▶│ Manifest │────▶│ │
│ (ts-morph) │ │ Extractor │ │ Drift Detector │──▶ Drift Report
│ │ └──────────────┘ │ │
└──────────────┘ └────────┬────────┘
│
┌─────────────┐ ┌──────────────┐ │
│ Figma File │────▶│ Snapshot │──────────────┘
│ (MCP tools) │ │ Extractor │
│ │ └──────────────┘
└──────────────┘
│ ┌─────────────────┐
│◀─────────────────────────────────│ Code→Figma Sync │
│ │ (use_figma API) │
│ └─────────────────┘
│
│──────────────────────────────────▶┌─────────────────┐
│ Figma→Code Sync │
│ (AST rewrite) │
└─────────────────┘
- Manifest Extraction — Parses React component source via ts-morph to extract variants (via a pluggable adapter: cva, tailwind-variants, Emotion, styled-components, or CSS Modules), TypeScript props, color token references, spacing classes, and radius classes
- Figma Snapshot — Fetches component structure from Figma via
get_design_context, extracts variant properties, color bindings (token-bound or hardcoded), auto-layout spacing, and corner radius - Drift Detection — Compares manifests vs snapshots, produces a categorized report:
color— token mismatch or hardcoded vs token-boundspacing— gap, padding, or item-spacing mismatchradius— corner radius mismatchvariant— variant exists in code but not Figma, or vice versaprop— prop exists in code but not mapped in Figma
- Sync — Code→Figma generates chunked
use_figmaPlugin API scripts (respecting the 20KB output limit). Figma→Code modifies the component AST via ts-morph. - Story Generation — Produces Storybook stories with
playfunctions usingexpect,within,userEventfromstorybook/test
figma-drift started as a shadcn/ui-focused tool (cva + Tailwind + OKLCH). The extraction layer is now pluggable so non-shadcn stacks work too. Defaults are unchanged, so existing shadcn/cva/Tailwind/OKLCH projects behave exactly as before.
Variant extraction — auto-detected per file (cva tried first for backward
compatibility, then the others). Override with the variantExtractor option on
extractManifest:
| Extractor | Recognizes |
|---|---|
cva |
cva(base, { variants, defaultVariants }) (class-variance-authority) |
tailwind-variants |
tv({ variants, defaultVariants }) |
emotion |
Emotion CSS-in-JS: @emotion/styled templates and @emotion/react css\`/css({ … }), e.g. ${p => p.variant === 'x' ? … : …}` |
styled-components |
prop-conditional interpolation, e.g. ${p => p.variant === 'x' ? … : …} |
css-modules |
styles.* keys imported from a *.module.css/scss file |
import { extractManifest, extractVariants } from '@effinrich/figma-drift'
extractManifest('Button.tsx') // auto-detect
extractManifest('Button.tsx', { variantExtractor: 'css-modules' }) // forceSpacing / radius — beyond the built-in Tailwind maps, tailwindToPx now
parses arbitrary values (p-[13px], gap-[1.5rem], rounded-[6px]), resolves
a project tailwind.config.{js,cjs,json} spacing/borderRadius scale (merged over
the defaults), and accepts raw CSS lengths / var(--token) for CSS-in-JS and
CSS Modules stacks.
import { resolveSpacingRadiusMaps, tailwindToPx } from '@effinrich/figma-drift'
const { spacingMap, radiusMap } = resolveSpacingRadiusMaps({ projectDir: process.cwd() })
tailwindToPx('p-13', { spacingMap, radiusMap }) // custom scale from tailwind.config
tailwindToPx('gap-[1.5rem]') // arbitrary value → 24
tailwindToPx('var(--md)', { tokenMap: { '--md': '8px' } }) // raw CSS token → 8Color tokens — parsing accepts OKLCH, HSL/HSLA, RGB/RGBA, hex, and named
colors (via culori). Tokens can be sourced from Tailwind
color classes or from a JS/TS/JSON theme object (a colors map or an MUI-style
theme.palette) via extractThemeObjectTokens / flattenThemeColors.
figma-drift also syncs design tokens between CSS custom properties and Figma variables:
- Parses
:rootand.darkblocks from your CSS for token values in any supported color format (OKLCH, HSL, RGB/RGBA, hex) - Also supports theme objects (
extractThemeObjectTokens) for styled-system / theme-ui / MUI palettes - Fetches Figma variable definitions via
get_variable_defs - Normalizes both to hex for comparison (using culori)
- Syncs in either direction: update Figma variables from CSS, or update CSS from Figma
| Tool | Purpose |
|---|---|
get_design_context |
Fetch structured design data for a component |
get_metadata |
Fallback for large components (>20KB) — get node map first |
get_screenshot |
Visual reference for validation |
get_variable_defs |
Fetch design token variable definitions |
search_design_system |
Match code components to Figma components |
use_figma |
Write changes to Figma canvas via Plugin API |
By default (autoSync: false), figma-drift shows the drift report and waits for you to choose a direction per component. This is the safe default — opacity differences like destructive/10 vs solid destructive are judgment calls that benefit from human review.
Set autoSync: true with a preferDirection to sync automatically:
{
"fileKey": "YOUR_FILE_KEY",
"autoSync": true,
"preferDirection": "code-to-figma"
}This is useful for teams where code is the source of truth and Figma should always match.
The component map links each React component file to its Figma counterpart:
{
"version": 1,
"figmaFileKey": "YOUR_FILE_KEY",
"entries": [
{
"filePath": "src/components/ui/button.tsx",
"figmaNodeId": "17:14",
"figmaPageName": "Atoms",
"componentName": "Button",
"lastSyncedAt": "2025-01-15T10:30:00.000Z",
"lastSyncDirection": "code-to-figma"
}
]
}All operations are logged:
[2025-01-15T10:30:00.000Z] SYNC component=Button direction=code-to-figma result=success
[2025-01-15T10:30:01.000Z] STORY component=Button result=success path=src/stories/atoms/Button.stories.tsx
[2025-01-15T10:30:02.000Z] DRIFT components=14 in-sync=11 drifted=2 unlinked=1
If you're using Kiro, figma-drift provides hooks for automatic sync:
- component-drift-check —
fileEditedon component files → auto drift detection - full-drift-scan —
userTriggered→ full drift scan across all components - figma-pull —
userTriggered→ pull Figma changes into code - post-sync-stories —
postTaskExecution→ auto-generate stories after sync
- 20KB output limit per
use_figmacall — large operations are automatically chunked - No image support in
use_figmayet — images use placeholder rectangles - No custom fonts in Figma write operations — Inter is used as fallback
- Code Connect requires Organization or Enterprise Figma plan — figma-drift uses component descriptions as metadata on Pro plans
- Non-cva stacks are best-effort — cva and tailwind-variants are extracted precisely; Emotion, styled-components, and CSS Modules variant detection is heuristic (see Framework-Agnostic Adapters)
This project uses the Oxc toolchain (Rust-based) for linting and formatting instead of ESLint/Prettier:
| Script | Command | Purpose |
|---|---|---|
npm run lint |
oxlint |
Lint with oxlint (config: .oxlintrc.json) |
npm run format |
oxfmt |
Format with oxfmt (config: .oxfmtrc.json) |
npm run format:check |
oxfmt --check |
Verify formatting without writing |
npm run typecheck |
tsc --noEmit |
Type-check only (no emit) |
npm run test |
vitest --run |
Run the test suite |
npm run build |
tsup |
Build the library |
MIT