Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Set up a local development workflow for Figma plugin and REST API projects. Use when building Figma plugins, creating design-to-code pipelines, or developing against the Figma API with hot reload. Trigger with phrases like "figma dev setup", "figma plugin development", "figma local development", "develop figma plugin".
.claude/skills/jeremylongshore-figma-local-dev-loop/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -3% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 18% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 162% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 41% | 0% |
Set up fast local development for two workflows: building Figma plugins that run inside the Figma editor, and building external apps that consume the Figma REST API.
FIGMA_PAT configured (see figma-install-auth)figma-integration/
├── src/
│ ├── figma-client.ts # Shared fetch wrapper
│ ├── extract-tokens.ts # Design token extraction
│ └── export-assets.ts # Asset export pipeline
├── tests/
│ ├── figma-client.test.ts
│ └── fixtures/ # Saved API responses for offline testing
│ └── sample-file.json
├── .env.local # FIGMA_PAT, FIGMA_FILE_KEY (git-ignored)
├── .env.example # Template for team
├── tsconfig.json
└── package.jsonmy-figma-plugin/
├── manifest.json # Plugin manifest (required by Figma)
├── code.ts # Plugin backend (runs in sandbox)
├── ui.html # Plugin UI (runs in iframe)
├── package.json
└── tsconfig.jsonmanifest.json (required):
json{ "name": "My Plugin", "id": "1234567890", "api": "1.0.0", "main": "dist/code.js", "ui": "dist/ui.html", "editorType": ["figma"], "permissions": ["currentuser"] }
json{ "scripts": { "build": "esbuild code.ts --bundle --outfile=dist/code.js --target=es2020", "watch": "esbuild code.ts --bundle --outfile=dist/code.js --target=es2020 --watch", "dev": "concurrently \"npm run watch\" \"npm run watch:ui\"", "watch:ui": "esbuild ui.tsx --bundle --outfile=dist/ui.html --loader:.html=copy --watch" }, "devDependencies": { "@figma/plugin-typings": "^1.0.0", "esbuild": "^0.20.0", "typescript": "^5.0.0" } }
Load the plugin in Figma:
manifest.jsonnpm run watch -- changes auto-reloadjson{ "scripts": { "dev": "tsx watch src/extract-tokens.ts", "test": "vitest", "test:watch": "vitest --watch" } }
typescript// tests/figma-client.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { readFileSync } from 'fs'; // Load a saved API response for offline testing const sampleFile = JSON.parse( readFileSync('tests/fixtures/sample-file.json', 'utf-8') ); describe('Figma token extraction', () => { beforeEach(() => { // Mock fetch to return saved fixture vi.spyOn(global, 'fetch').mockResolvedValue( new Response(JSON.stringify(sampleFile), { status: 200 }) ); }); it('should extract color styles from file', async () => { const res = await fetch('https://api.figma.com/v1/files/test-key'); const file = await res.json(); const styles = Object.values(file.styles); expect(styles.length).toBeGreaterThan(0); }); });
bash# Snapshot a Figma file for offline testing curl -s -H "X-Figma-Token: ${FIGMA_PAT}" \ "https://api.figma.com/v1/files/${FIGMA_FILE_KEY}" \ > tests/fixtures/sample-file.json # Snapshot specific nodes curl -s -H "X-Figma-Token: ${FIGMA_PAT}" \ "https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/nodes?ids=0:1,0:2" \ > tests/fixtures/sample-nodes.json
| Error | Cause | Solution | |-------|-------|----------| | Plugin not appearing in Figma | Wrong manifest path | Re-import from correct manifest.json | | figma global undefined | Running outside Figma sandbox | Use @figma/plugin-typings for types only | | Fixture stale | File changed since snapshot | Re-run fixture download script | | esbuild watch crash | Syntax error in TS | Fix error; watch auto-restarts |
typescript// code.ts -- minimal Figma plugin figma.showUI(__html__, { width: 300, height: 200 }); figma.ui.onmessage = (msg: { type: string; count: number }) => { if (msg.type === 'create-rectangles') { for (let i = 0; i < msg.count; i++) { const rect = figma.createRectangle(); rect.x = i * 150; rect.fills = [{ type: 'SOLID', color: { r: 1, g: 0.5, b: 0 } }]; figma.currentPage.appendChild(rect); } figma.closePlugin(); } };
See figma-sdk-patterns for production-ready code patterns.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 32,714 | 16,995 | -48% | 1 | 1 | 0% | 4,867 | 4,736 | -3% | 0 | 0 | — |
case-02 | fail→pass | 19,254 | 16,975 | -12% | 1 | 1 | 0% | 3,795 | 4,480 | +18% | 0 | 0 | — |
case-03 | fail→fail | 24,347 | 47,617 | +96% | 1 | 1 | 0% | 4,748 | 5,861 | +23% | 0 | 0 | — |
case-04 | pass→pass | 25,844 | 27,205 | +5% | 1 | 1 | 0% | 5,094 | 7,251 | +42% | 0 | 0 | — |
case-05 | pass→pass | 15,719 | 14,869 | -5% | 1 | 1 | 0% | 2,701 | 3,723 | +38% | 0 | 0 | — |
case-06 | pass→fail | 13,954 | 18,591 | +33% | 1 | 1 | 0% | 2,795 | 5,418 | +94% | 0 | 0 | — |
case-07 | fail→pass | 6,506 | 3,050 | -53% | 1 | 1 | 0% | 1,243 | 2,172 | +75% | 0 | 0 | — |
case-08 | pass→pass | 7,641 | 3,789 | -50% | 1 | 1 | 0% | 1,387 | 2,309 | +66% | 0 | 0 | — |
case-09 | fail→pass | 4,395 | 3,742 | -15% | 1 | 1 | 0% | 915 | 2,393 | +162% | 0 | 0 | — |
case-10 | pass→pass | 241,962 | 4,334 | -98% | 1 | 1 | 0% | 1,521 | 2,343 | +54% | 0 | 0 | — |
case-11 | pass→pass | 13,944 | 7,621 | -45% | 1 | 1 | 0% | 1,970 | 2,807 | +42% | 0 | 0 | — |
case-12 | fail→pass | 13,867 | 10,221 | -26% | 1 | 1 | 0% | 2,305 | 3,253 | +41% | 0 | 0 | — |
case-13 | fail→fail | 19,160 | 11,619 | -39% | 1 | 1 | 0% | 3,718 | 3,742 | +1% | 0 | 0 | — |
case-14 | pass→pass | 3,997 | 2,878 | -28% | 1 | 1 | 0% | 718 | 1,975 | +175% | 0 | 0 | — |
case-15 | pass→pass | 5,780 | 2,828 | -51% | 1 | 1 | 0% | 815 | 2,116 | +160% | 0 | 0 | — |
case-16 | pass→pass | 4,625 | 3,093 | -33% | 1 | 1 | 0% | 856 | 2,087 | +144% | 0 | 0 | — |
case-17 | pass→pass | 8,194 | 4,648 | -43% | 1 | 1 | 0% | 1,570 | 2,338 | +49% | 0 | 0 | — |
case-18 | pass→pass | 8,729 | 5,703 | -35% | 1 | 1 | 0% | 1,590 | 2,546 | +60% | 0 | 0 | — |
case-19 | fail→pass | 11,017 | 5,846 | -47% | 1 | 1 | 0% | 1,904 | 2,634 | +38% | 0 | 0 | — |
case-20 | fail→pass | 12,298 | 8,982 | -27% | 1 | 1 | 0% | 1,847 | 3,101 | +68% | 0 | 0 | — |
case-21 | fail→pass | 7,686 | 4,227 | -45% | 1 | 1 | 0% | 1,264 | 2,256 | +78% | 0 | 0 | — |
case-22 | fail→pass | 17,725 | 7,777 | -56% | 1 | 1 | 0% | 3,265 | 2,971 | -9% | 0 | 0 | — |
case-23 | pass→pass | 9,807 | 4,284 | -56% | 1 | 1 | 0% | 1,897 | 2,366 | +25% | 0 | 0 | — |
case-24 | pass→pass | 9,332 | 8,886 | -5% | 1 | 1 | 0% | 1,593 | 3,075 | +93% | 0 | 0 | — |
case-25 | fail→fail | 10,992 | 10,244 | -7% | 1 | 1 | 0% | 2,130 | 3,618 | +70% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 25 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.