Convert linear Figma screen flows into pixel-perfect React components with Tailwind CSS. Creates fully functional screens with iOS-native animations, interactive elements, and automated visual verification. Use when converting Figma mobile flows to React, building demo apps from designs, or replicating vendor UIs (like Plaid, Stripe, etc.). Requires figma MCP server and dev-browser skill.
This skill is limited to using the following tools:
Convert linear Figma screen flows into pixel-perfect, fully functional React components.
This skill uses a manifest file as the single source of truth. The manifest:
CRITICAL: Write the manifest FIRST, then update it as you progress. Never skip updating the manifest.
Use AskUserQuestion to get the Figma URL (required):
https://www.figma.com/design/{fileKey}/{fileName}https://www.figma.com/design/{fileKey}/{fileName}?node-id=1-234Run these detections automatically:
1. SCREENS: Call mcp__figma__get_metadata(fileKey, nodeId)
→ Filter for screen-like frames (consistent dimensions)
→ Order by x-position (left-to-right) or by name
→ Derive component names from layer names
2. FLOW NAME: Derive from Figma file/frame name (kebab-case)
3. OUTPUT DIR: Glob for src/, match project structure patterns
4. ASSET DIR: Look for public/ or src/assets/
5. DEVICE FRAME: Glob for **/DeviceFrame.tsx, **/PhoneFrame.tsx
→ null if not found
6. CONTAINER MODE: Infer from Figma frame dimensions
→ 390x844 or similar = phone-frame
→ Has overlay/backdrop = modal
→ Otherwise = fullscreen
MANDATORY: Present all detected values for user confirmation before creating manifest.
Use AskUserQuestion to show detected configuration:
I've analyzed the Figma file and your project. Please confirm or adjust:
┌─────────────────────────────────────────────────────────────────┐
│ DETECTED CONFIGURATION │
├─────────────────────────────────────────────────────────────────┤
│ Flow name: plaid-link (from Figma frame name) │
│ Output dir: src/plaid/ (matches src/{feature}/ pattern) │
│ Asset dir: public/plaid-assets/ │
│ Device frame: src/components/DeviceFrame.tsx (found) │
│ Container: phone-frame (390x844 frames detected) │
├─────────────────────────────────────────────────────────────────┤
│ SCREENS DETECTED (5) │
├─────────────────────────────────────────────────────────────────┤
│ 1. "01 - Welcome" (1:234) → WelcomeScreen.tsx │
│ 2. "02 - Select Bank" (1:567) → SelectBankScreen.tsx │
│ 3. "03 - Credentials" (1:890) → CredentialsScreen.tsx │
│ 4. "04 - Loading" (1:901) → LoadingScreen.tsx │
│ 5. "05 - Success" (1:912) → SuccessScreen.tsx │
└─────────────────────────────────────────────────────────────────┘
Options:
1. Proceed with detected configuration
2. Adjust settings (I'll ask follow-up questions)
If user chooses "Adjust settings", ask specific follow-up questions:
Only after user confirms configuration, create {flow-name}-manifest.json.
Why a manifest? The manifest is important because it organizes and tracks progress throughout the process so the agent does not have to rely on their context window.
{
"meta": {
"flowName": "plaid-link",
"figmaFileKey": "abc123",
"figmaUrl": "https://www.figma.com/design/abc123/Plaid-Flow",
"createdAt": "2024-12-19T10:00:00Z",
"currentPhase": "extraction"
},
"config": {
"outputDir": "src/plaid",
"assetDir": "public/plaid-assets",
"containerMode": "phone-frame",
"deviceFrame": "src/components/DeviceFrame.tsx"
},
"screens": [
{
"order": 1,
"figma": {
"nodeId": "1:234",
"url": "https://www.figma.com/design/abc123/Plaid?node-id=1-234",
"layerName": "01 - Welcome"
},
"react": {
"componentName": "WelcomeScreen",
"filePath": "src/plaid/screens/components/WelcomeScreen.tsx",
"registryId": "welcome"
},
"extraction": {
"status": "pending",
"screenshot": null,
"designContext": null
},
"generation": {
"status": "blocked"
},
"verification": {
"status": "blocked",
"attempts": 0,
"passed": false
},
"assetRefs": []
}
],
"assets": [],
"files": {
"registry": { "path": "src/plaid/screens/registry.ts", "status": "pending" },
"demoPage": { "path": "src/plaid/PlaidDemoPage.tsx", "status": "pending" },
"route": { "path": "src/App.tsx", "routePath": "/plaid", "status": "pending" }
},
"phases": {
"config": { "status": "complete" },
"extraction": { "status": "in_progress" },
"assets": { "status": "blocked" },
"architecture": { "status": "blocked" },
"screens": { "status": "blocked" },
"verification": { "status": "blocked" }
}
}
Display configuration summary and screen list. Get explicit confirmation before proceeding.
Configuration:
Flow: plaid-link
Screens: 5 discovered
Output: src/plaid/
Assets: public/plaid-assets/
Screens to convert:
1. 01 - Welcome (1:234) → WelcomeScreen.tsx
2. 02 - Select Bank (1:567) → SelectBankScreen.tsx
3. 03 - Credentials (1:890) → CredentialsScreen.tsx
...
Proceed? [Y/n]
READ manifest
VERIFY phases.config.status === "complete"
IF NOT → STOP, complete Phase 1
For each screen in manifest.screens:
1. Get screenshot:
mcp__figma__get_screenshot(fileKey, nodeId)
→ Save to temp location
→ UPDATE manifest: screens[i].extraction.screenshot = path
2. Get design context:
mcp__figma__get_design_context(fileKey, nodeId)
→ Returns: Tailwind classes, asset URLs, SVG code
→ UPDATE manifest: screens[i].extraction.designContext = "extracted"
→ UPDATE manifest: screens[i].extraction.status = "complete"
3. Identify assets from design context:
→ For each asset found, add to manifest.assets[] if not exists
→ Add asset ID to screens[i].assetRefs[]
After ALL screens extracted:
UPDATE manifest: phases.extraction.status = "complete"
UPDATE manifest: phases.assets.status = "in_progress"
CHECKPOINT: Read manifest. Every screen must have extraction.status === "complete"
READ manifest
VERIFY phases.extraction.status === "complete"
VERIFY ALL screens[].extraction.status === "complete"
IF NOT → STOP, complete Phase 2
For each asset in manifest.assets, try extraction methods in order until one succeeds:
1. ATTEMPT: Direct extraction from design context
- SVGs: Copy EXACT SVG code
- Images: curl -L "{figmaUrl}" -o {localPath}
IF success:
assets[i].status = "verified"
assets[i].extractionMethod = "direct"
CONTINUE to next asset
2. ATTEMPT: Figma REST API (if available)
curl -H "X-Figma-Token: {token}" \
"https://api.figma.com/v1/images/{fileKey}?ids={nodeId}&format=png&scale=2"
IF success:
assets[i].status = "verified"
assets[i].extractionMethod = "api"
CONTINUE to next asset
3. ATTEMPT: Screenshot extraction (images only, not SVGs)
- Crop asset region from full screen screenshot
- Save to local path
IF success AND asset is NOT svg:
assets[i].status = "degraded"
assets[i].extractionMethod = "screenshot"
CONTINUE to next asset
4. ALL METHODS FAILED:
assets[i].status = "failed"
assets[i].failureReason = "All extraction methods failed: {details}"
CONTINUE to next asset (don't stop)
Note: If any assets get to step 2 (Figma REST API), stop and ask user if they can share a token before proceeding.
After attempting ALL assets (regardless of success/failure):
UPDATE manifest: phases.assets.status = "complete"
UPDATE manifest: phases.architecture.status = "in_progress"
Note: Failed assets don't block progress. They get placeholder code in Phase 5 and are reported in the final summary.
READ manifest
VERIFY phases.assets.status === "complete"
IF NOT → STOP, complete Phase 3
Write {outputDir}/screens/registry.ts:
import type { ComponentType } from 'react';
export interface ScreenProps {
onNext?: () => void;
onBack?: () => void;
onClose?: () => void;
}
export interface Screen {
id: string;
title: string;
component: ComponentType<ScreenProps>;
}
// Imports generated from manifest.screens
import { WelcomeScreen } from './components/WelcomeScreen';
// ... for each screen
export const screens: Screen[] = [
{ id: 'welcome', title: 'Welcome', component: WelcomeScreen },
// ... from manifest
];
export const screenFlow: string[] = ['welcome', /* ... */];
export function getScreenById(id: string): Screen | undefined {
return screens.find(s => s.id === id);
}
export function getNextScreenId(currentId: string): string | null {
const index = screenFlow.indexOf(currentId);
return index >= 0 && index < screenFlow.length - 1 ? screenFlow[index + 1] : null;
}
export function getPrevScreenId(currentId: string): string | null {
const index = screenFlow.indexOf(currentId);
return index > 0 ? screenFlow[index - 1] : null;
}
UPDATE manifest: files.registry.status = "complete"
Write {outputDir}/{FlowName}DemoPage.tsx:
import { useState } from 'react';
import { screens, screenFlow, getScreenById, getNextScreenId, getPrevScreenId } from './screens/registry';
import { DeviceFrame } from '@/components/DeviceFrame';
export function PlaidDemoPage() {
const searchParams = new URLSearchParams(window.location.search);
const directScreen = searchParams.get('screen');
const [currentScreenId, setCurrentScreenId] = useState(
directScreen && screenFlow.includes(directScreen) ? directScreen : screenFlow[0]
);
const currentScreen = getScreenById(currentScreenId);
if (!currentScreen) return null;
const CurrentComponent = currentScreen.component;
const handleNext = () => {
const next = getNextScreenId(currentScreenId);
if (next) setCurrentScreenId(next);
};
const handleBack = () => {
const prev = getPrevScreenId(currentScreenId);
if (prev) setCurrentScreenId(prev);
};
const handleClose = () => setCurrentScreenId(screenFlow[0]);
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-8">
<DeviceFrame>
<CurrentComponent onNext={handleNext} onBack={handleBack} onClose={handleClose} />
</DeviceFrame>
{/* Screen selector sidebar */}
<div className="ml-8 space-y-2">
{screens.map((screen, i) => (
<button
key={screen.id}
onClick={() => setCurrentScreenId(screen.id)}
className={`block text-left px-3 py-1 rounded ${
screen.id === currentScreenId ? 'bg-blue-500 text-white' : 'text-gray-600 hover:bg-gray-200'
}`}
>
{i + 1}. {screen.title}
</button>
))}
</div>
</div>
);
}
UPDATE manifest: files.demoPage.status = "complete"
Add route to App.tsx or router config.
UPDATE manifest: files.route.status = "complete"
UPDATE manifest: phases.architecture.status = "complete"
UPDATE manifest: phases.screens.status = "in_progress"
UPDATE manifest: ALL screens[].generation.status = "pending" (unblock them)
READ manifest
VERIFY phases.architecture.status === "complete"
VERIFY files.registry.status === "complete"
IF NOT → STOP, complete Phase 4
For each screen in manifest.screens:
Before generating, gather required assets:
READ manifest
FOR assetId IN screen.assetRefs:
asset = manifest.assets.find(a => a.id === assetId)
IF asset.status === "verified" OR asset.status === "degraded":
→ Use asset.local.filePath in code
IF asset.status === "failed":
→ Use visible placeholder (gray box with "MISSING: {name}")
→ Add to screen's failedAssets list for summary
Generate component with real assets or visible placeholders:
import type { ScreenProps } from '../registry';
export function WelcomeScreen({ onNext, onBack, onClose }: ScreenProps) {
return (
<div className="flex flex-col h-full bg-white">
{/* Use EXACT Tailwind classes from Figma design context */}
{/* Use EXACT SVG code from Figma */}
{/* Verified asset - use actual file */}
<img src="/plaid-assets/plaid-logo.svg" alt="Plaid" className="w-[120px] h-[40px]" />
{/* Failed asset - visible placeholder so user sees what's missing */}
<div className="w-[48px] h-[48px] bg-red-100 border-2 border-red-300 border-dashed flex items-center justify-center">
<span className="text-[10px] text-red-500 text-center">MISSING:<br/>bank-chip</span>
</div>
</div>
);
}
Placeholders are intentionally ugly — red dashed borders make missing assets obvious when viewing the app, rather than silently broken.
Update manifest after each screen:
UPDATE manifest: screens[i].generation.status = "complete"
READ manifest
VERIFY ALL screens[].generation.status === "complete"
UPDATE manifest: phases.screens.status = "complete"
UPDATE manifest: phases.verification.status = "in_progress"
UPDATE manifest: ALL screens[].verification.status = "pending"
READ manifest
VERIFY phases.screens.status === "complete"
IF NOT → STOP, complete Phase 5
Ensure dev server is running.
For each screen in manifest.screens:
1. Navigate to /{flow}?screen={registryId}
2. Wait for render + asset loading
3. Screenshot at device dimensions
4. Compare against Figma screenshot (from extraction phase)
5. IF discrepancies found:
a. Identify issue (spacing, color, border-radius, etc.)
b. Auto-fix if possible (Tailwind class adjustment)
c. Re-screenshot and compare
d. Repeat up to 10 times
e. UPDATE manifest: screens[i].verification.attempts++
6. UPDATE manifest:
- Pass: screens[i].verification.status = "passed", .passed = true
- Fail after 10 attempts: screens[i].verification.status = "failed"
Add issues to screens[i].verification.issues[]
UPDATE manifest: phases.verification.status = "complete"
UPDATE manifest: meta.currentPhase = "complete"
Read manifest and generate summary:
✅ Flow: plaid-link
Route: /plaid
Screens: 5
📁 Files Created:
src/plaid/screens/registry.ts
src/plaid/screens/components/WelcomeScreen.tsx
src/plaid/screens/components/SelectBankScreen.tsx
...
src/plaid/PlaidDemoPage.tsx
🎨 Assets (8 total, 6 verified, 1 degraded, 1 failed):
✅ plaid-logo.svg (verified, direct)
✅ success-check.svg (verified, direct)
✅ close-icon.svg (verified, direct)
✅ back-arrow.svg (verified, direct)
✅ flagstar-logo.png (verified, direct)
✅ bank-illustration.png (verified, api)
⚠️ search-icon.svg (degraded, screenshot) — lower quality
❌ bank-chip-chase.png (failed) — variant mismatch, all methods failed
🖼️ Screen Verification:
✅ WelcomeScreen (passed)
✅ SelectBankScreen (passed, auto-fixed: rounded-lg → rounded-xl)
⚠️ CredentialsScreen (passed with placeholder - 1 missing asset)
✅ SuccessScreen (passed)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 ACTION REQUIRED (1 failed asset):
bank-chip-chase.png
├─ Figma node: https://www.figma.com/design/abc123/Plaid?node-id=1-890
├─ Reason: Component variant - get_design_context returned base variant
├─ Used in: CredentialsScreen (currently showing red placeholder)
└─ Fix: Export manually from Figma → public/plaid-assets/bank-chip-chase.png
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 Manifest: plaid-link-manifest.json
Run: pnpm dev
Visit: http://localhost:5173/plaid
The summary shows exactly what succeeded, what failed, and what the user needs to do. Failed assets have direct links to the Figma node for easy manual export.
interface Manifest {
meta: {
flowName: string;
figmaFileKey: string;
figmaUrl: string;
createdAt: string;
currentPhase: 'config' | 'extraction' | 'assets' | 'architecture' | 'screens' | 'verification' | 'complete';
};
config: {
outputDir: string;
assetDir: string;
containerMode: 'phone-frame' | 'modal' | 'fullscreen' | 'none';
deviceFrame: string | null;
};
screens: Array<{
order: number;
figma: {
nodeId: string;
url: string;
layerName: string;
};
react: {
componentName: string;
filePath: string;
registryId: string;
};
extraction: {
status: 'pending' | 'complete';
screenshot: string | null;
designContext: 'pending' | 'extracted';
};
generation: {
status: 'blocked' | 'pending' | 'complete';
};
verification: {
status: 'blocked' | 'pending' | 'passed' | 'failed';
attempts: number;
passed: boolean;
issues: string[];
};
assetRefs: string[];
}>;
assets: Array<{
id: string;
figma: {
nodeId: string;
layerName: string;
url: string;
};
local: {
fileName: string;
filePath: string;
type: 'svg' | 'png' | 'jpg';
};
status: 'pending' | 'downloading' | 'verified' | 'degraded' | 'failed';
extractionMethod: 'direct' | 'api' | 'screenshot' | null;
failureReason: string | null;
usedBy: string[];
}>;
files: {
registry: { path: string; status: 'pending' | 'complete' };
demoPage: { path: string; status: 'pending' | 'complete' };
route: { path: string; routePath: string; status: 'pending' | 'complete' };
};
phases: {
config: { status: 'pending' | 'in_progress' | 'complete' };
extraction: { status: 'blocked' | 'in_progress' | 'complete' };
assets: { status: 'blocked' | 'in_progress' | 'complete' };
architecture: { status: 'blocked' | 'in_progress' | 'complete' };
screens: { status: 'blocked' | 'in_progress' | 'complete' };
verification: { status: 'blocked' | 'in_progress' | 'complete' };
};
}
| Phase | Gate Check |
|---|---|
| 2. Extraction | phases.config === "complete" |
| 3. Assets | phases.extraction === "complete" AND all screens[].extraction.status === "complete" |
| 4. Architecture | phases.assets === "complete" (assets always complete - failures don't block) |
| 5. Screens | phases.architecture === "complete" AND files.registry.status === "complete" |
| 6. Verification | phases.screens === "complete" AND all screens[].generation.status === "complete" |
mcp__figma__get_metadata(fileKey, nodeId) → Screen structure
mcp__figma__get_screenshot(fileKey, nodeId) → Visual reference
mcp__figma__get_design_context(fileKey, nodeId) → Code + assets
/* Navigation */ cubic-bezier(0.36, 0.66, 0.04, 1) 500ms
/* Modal */ cubic-bezier(0.32, 0.72, 0, 1) 500ms