Skip to content

About

Bidirectional drift detection and sync between React components and Figma design systems.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

@effinrich/figma-drift

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.

What It Does

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)

Prerequisites

Figma MCP Server

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.

Project Requirements

  • 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)

Optional

  • Storybook (latest version recommended) — for auto-generated story files with interaction tests

Install

npm install @effinrich/figma-drift

MCP Server

figma-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.

Configuration

Create a .figma-drift.json in your project root. Only fileKey is required — everything else has sensible defaults:

{
  "fileKey": "YOUR_FILE_KEY"
}

Full Configuration Reference

{
  "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

Alternative Configuration Methods

Configuration is resolved in priority order:

  1. .figma-drift.json in project root
  2. "figmaDrift" field in package.json
  3. FIGMA_DRIFT_FILE_KEY environment variable
# Environment variable
export FIGMA_DRIFT_FILE_KEY="YOUR_FILE_KEY"
// package.json
{
  "figmaDrift": {
    "fileKey": "YOUR_FILE_KEY",
    "componentDirs": ["src/ui", "src/features"]
  }
}

Initialize the Component Map

npx @effinrich/figma-drift init

This scans the directories in componentDirs, matches components to Figma via search_design_system, and saves the map to componentMapPath.

CLI Usage

# 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 changes

Programmatic API

import {
  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,
});

How It Works

Architecture

┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
│  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)   │
                                            └─────────────────┘

Pipeline Steps

  1. 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
  2. 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
  3. Drift Detection — Compares manifests vs snapshots, produces a categorized report:
    • color — token mismatch or hardcoded vs token-bound
    • spacing — gap, padding, or item-spacing mismatch
    • radius — corner radius mismatch
    • variant — variant exists in code but not Figma, or vice versa
    • prop — prop exists in code but not mapped in Figma
  4. Sync — Code→Figma generates chunked use_figma Plugin API scripts (respecting the 20KB output limit). Figma→Code modifies the component AST via ts-morph.
  5. Story Generation — Produces Storybook stories with play functions using expect, within, userEvent from storybook/test

Framework-Agnostic Adapters

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' }) // force

Spacing / 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 → 8

Color 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.

Design Token Sync

figma-drift also syncs design tokens between CSS custom properties and Figma variables:

  • Parses :root and .dark blocks 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

Figma MCP Tools Used

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

Sync Behavior

Prompt Mode (default)

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.

Auto Mode

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.

Component Map

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"
    }
  ]
}

Sync Logging

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

Kiro Hook Integration

If you're using Kiro, figma-drift provides hooks for automatic sync:

  • component-drift-check — fileEdited on 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

Limitations

  • 20KB output limit per use_figma call — large operations are automatically chunked
  • No image support in use_figma yet — 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)

Development

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

License

MIT

About

Bidirectional drift detection and sync between React components and Figma design systems.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages