EmpireUI
Get Pro
← Blog8 min read#figma-api#react-components#design-tokens

Figma API to React: Auto-Generating Components from Designs

Stop hand-coding what Figma already drew. Learn how to pull design tokens and components directly from the Figma API and generate production React code automatically.

A designer's screen showing Figma component layers alongside a code editor with React component output

Why Hand-Translating Figma Designs Is Wasting Your Sprint

Honestly, every hour you spend pixel-peeking a Figma frame and typing out JSX by hand is an hour the design-to-code gap quietly grows wider. Designers iterate. They swap the border-radius from 8px to 12px, change the primary color from #6366f1 to #7c3aed, and nudge spacing by half a grid unit. By Friday your React components are already stale.

The Figma REST API has been stable enough for production use since around Figma v116, and yet most teams still treat it as a curiosity — something you glance at once and forget. That's a mistake. With a few hundred lines of Node.js you can build a pipeline that reads your Figma file, extracts design tokens, and emits TypeScript React components that stay honest with the source of truth.

This article walks through that pipeline. We're going to use the Figma API directly — no third-party plugin subscriptions required — so you understand what's actually happening under the hood. You'll end up with something you can extend, version, and run in CI.

Setting Up Figma API Access and File Parsing

First things first: you need a personal access token from Figma's developer settings (figma.com/settings → Security → Personal access tokens). Treat it like a password — store it in an environment variable, never commit it. The base URL for all REST calls is https://api.figma.com/v1/.

The two endpoints you'll lean on are /files/:file_key (full document tree) and /files/:file_key/styles (named styles). The file key is the long string in your Figma URL between /file/ and the title slug. Fetch the document once and cache it locally during development — the JSON blob for a moderately complex design system can easily exceed 2 MB.

Here's a minimal fetcher to get you started:

// figma-client.ts
const FIGMA_TOKEN = process.env.FIGMA_TOKEN!;
const BASE = 'https://api.figma.com/v1';

export async function fetchFigmaFile(fileKey: string) {
  const res = await fetch(`${BASE}/files/${fileKey}`, {
    headers: { 'X-Figma-Token': FIGMA_TOKEN },
  });
  if (!res.ok) throw new Error(`Figma API ${res.status}: ${res.statusText}`);
  return res.json() as Promise<FigmaFile>;
}

export async function fetchFigmaStyles(fileKey: string) {
  const res = await fetch(`${BASE}/files/${fileKey}/styles`, {
    headers: { 'X-Figma-Token': FIGMA_TOKEN },
  });
  if (!res.ok) throw new Error(`Figma API ${res.status}`);
  return res.json();
}

The FigmaFile type from @figma/rest-api-spec (published by Figma themselves) gives you typed access to the node tree. Install it with npm i -D @figma/rest-api-spec. It's not perfect — some union types are wide — but it's miles better than typing any everywhere.

Extracting Design Tokens: Colors, Typography, Spacing

The document tree is deeply nested. Every layer is a Node with a type field (FRAME, COMPONENT, TEXT, RECTANGLE, etc.) and optional fills, strokes, effects, and style properties. Walking it recursively to extract color tokens looks roughly like this:

// extract-tokens.ts
import type { Node, Paint } from '@figma/rest-api-spec';

type ColorToken = { name: string; hex: string; rgba: string };

function paintToHex(paint: Paint): string | null {
  if (paint.type !== 'SOLID' || !paint.color) return null;
  const { r, g, b, a = 1 } = paint.color;
  const toHex = (n: number) => Math.round(n * 255).toString(16).padStart(2, '0');
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}

export function extractColorTokens(node: Node, tokens: ColorToken[] = []): ColorToken[] {
  if ('fills' in node && Array.isArray(node.fills)) {
    for (const fill of node.fills) {
      if (fill.type === 'SOLID' && fill.color && node.name.startsWith('color/')) {
        const { r, g, b, a = 1 } = fill.color;
        tokens.push({
          name: node.name.replace('color/', '').replace(/\//g, '-'),
          hex: paintToHex(fill) ?? '',
          rgba: `rgba(${Math.round(r*255)}, ${Math.round(g*255)}, ${Math.round(b*255)}, ${a})`,
        });
      }
    }
  }
  if ('children' in node) {
    for (const child of node.children) extractColorTokens(child, tokens);
  }
  return tokens;
}

The naming convention matters a lot here. If your Figma layers follow a path like color/primary/500, the extractor above emits a token named primary-500. You'll want to agree on this convention with your design team before you build the pipeline — retrofitting it later hurts.

Typography tokens work similarly. Figma stores font family, weight, size, line height, and letter spacing on TEXT nodes under the style property. Extract the ones named text/* or whatever your team's convention is, then generate a CSS custom properties file or a Tailwind v4.0.2 @theme block.

Walking the Component Tree and Generating React Code

Color and typography tokens are the easy part. Actually generating React components from Figma COMPONENT nodes is where things get interesting — and where you'll need to make real trade-offs.

The approach that works best in practice is template-based generation, not AST manipulation. For each COMPONENT node you find, you read its children, infer semantic HTML (a FRAME with layoutMode: HORIZONTAL and primaryAxisAlignItems: SPACE_BETWEEN is almost certainly a flex row), then fill a handlebars or template-literal template.

// templates/button.tsx.template
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';

interface {{ComponentName}}Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}

export const {{ComponentName}} = forwardRef<HTMLButtonElement, {{ComponentName}}Props>(
  ({ className, variant = 'primary', size = 'md', ...props }, ref) => (
    <button
      ref={ref}
      className={cn(
        // base — 8px gap between icon and label
        'inline-flex items-center gap-2 rounded-{{borderRadius}} font-medium transition-colors',
        // variant
        variant === 'primary' && 'bg-{{primaryColor}} text-white hover:bg-{{primaryHover}}',
        variant === 'ghost' && 'bg-transparent border border-{{borderColor}} hover:bg-{{ghostHover}}',
        // size
        size === 'sm' && 'px-3 py-1.5 text-sm',
        size === 'md' && 'px-4 py-2 text-base',
        size === 'lg' && 'px-6 py-3 text-lg',
        className
      )}
      {...props}
    />
  )
);
{{ComponentName}}.displayName = '{{ComponentName}}';

The generator reads the COMPONENT node's corner radius ({{borderRadius}}), its fill color, and its hover state (you'll need a second variant COMPONENT named Button/hover by convention) and substitutes them into the template. It's not magic — it's string replacement — but it covers 80% of your UI components without any manual effort. If you're already using gradient backgrounds from your design system, the same token pipeline feeds those values in automatically.

Mapping Figma Styles to Tailwind CSS Classes

Here's the thing: Figma values and Tailwind v4.0.2 classes don't map 1-to-1 out of the box. A Figma shadow defined as x=0, y=4, blur=16, spread=0, color=rgba(0,0,0,0.12) doesn't have a matching shadow-* class unless you've added it to your theme. This is where your token pipeline and your Tailwind config need to be in sync.

The cleanest approach: generate a tailwind-tokens.css file that uses the new @theme block (Tailwind v4 style). Every color, shadow, and radius value from Figma lands in CSS custom properties, and Tailwind picks them up automatically. No manual extend keys in tailwind.config.ts.

For box shadows specifically, you'll want to read the effects array on Figma nodes. Each DROP_SHADOW effect gives you the raw numbers. Convert them to a CSS box-shadow string and write a custom property like --shadow-card: 0 4px 16px 0 rgba(0,0,0,0.12). Then in your Tailwind theme, shadow-card becomes a real utility. Check out the box-shadow reference guide if you need a refresher on the CSS property syntax before you start mapping values.

One gotcha: Figma's opacity on a fill and Figma's opacity on a layer are separate things. A fill with rgba(255,255,255,0.15) is different from a white fill on a layer with 15% opacity. Make sure your extractor reads fill.opacity and the layer's opacity property separately and combines them correctly, otherwise your glassmorphism panels will look wrong.

Running the Generator in CI and Keeping Sync Alive

A one-time script is fine for bootstrapping. But the real value is in running this in CI so that when a designer pushes changes to the Figma file, a PR opens automatically with the updated components. This is achievable with GitHub Actions and Figma's webhook API (available at POST /v2/webhooks), which fires a FILE_UPDATE event roughly 30 seconds after a file save.

The webhook handler (a tiny Edge Function or Lambda) receives the event, triggers a GitHub Actions workflow via the workflow_dispatch API, and the workflow runs your generator script, commits the diff to a branch, and opens a PR. The PR description can include a diff of which tokens changed — making design review actually useful.

Do think about what gets committed versus what gets generated at build time. Token CSS files belong in the repo (they're readable diffs). Generated component files are more controversial — if you've added custom logic to a component, the generator will overwrite it. The solution most teams land on is an // @generated comment at the top of auto-generated files and a rule that says hand-edited files live in a separate components/custom/ directory.

If you're also managing visual styles like glassmorphism effects or theme toggles, make sure your token pipeline exports both light and dark mode values. Figma's Modes API (the one tied to Variables, not the old Styles system) is the right place to pull those from — it's a separate endpoint at /files/:file_key/variables/local.

Handling Edge Cases: Variants, Auto Layout, and Nested Components

Figma's COMPONENT_SET node wraps all variants of a component. Each variant is a COMPONENT node with variantProperties like { State: 'hover', Size: 'lg' }. Your generator needs to read these and emit a union type for each property. Three variants with two properties each means you're generating a 2D matrix of combinations — handle that with a recursive property expander, not a hardcoded switch statement.

Auto Layout in Figma maps cleanly to CSS Flexbox and Grid. layoutMode: VERTICAL with itemSpacing: 16 is flex-col gap-4 in Tailwind (16px = gap-4 with 4px base). layoutMode: HORIZONTAL with counterAxisAlignItems: CENTER is flex-row items-center. The mapping table isn't long — write it once as a lookup object and you're done.

Nested components (instances of components inside other components) are represented as INSTANCE nodes with a componentId that points back to the master COMPONENT. You'll need a first pass over the document to build a componentId → generated component name map, then a second pass to replace nested INSTANCE nodes with JSX references. Skip this step and you end up with inline styles instead of <Button /> usage — which defeats the purpose.

Real-World Limitations and When Not to Automate

Let's be real: not everything in Figma should be auto-generated. Complex interactive components — accordions, date pickers, virtualized lists — have behavior that Figma can't express. The design file shows you what they look like in three states. It can't show you the keyboard navigation logic, the ARIA attributes, or the scroll position math.

The sweet spot for code generation is stateless or near-stateless components: buttons, badges, cards, avatars, input fields, typography scales, color swatches, and layout primitives. That's probably 60-70% of your component library by count, and getting those right automatically frees you to focus on the interactive 30% that actually needs hand-crafted logic.

Also consider the maturity of your design file. If your Figma file has inconsistent naming, mixed pixel values (some 8px gaps, some 9px, some 10px), and no proper component structure, the generator will faithfully reproduce all of that chaos as code. Garbage in, garbage out. Running a tailwind shadows audit on your design tokens before you start can surface those inconsistencies before they become bugs. Fix the design file first, then automate.

Track the generator's output over time. If the same component keeps getting manually edited after every generation run, that's a signal: either the template doesn't capture the needed behavior, or the design and code have legitimately diverged and need a conversation between designer and developer to reconcile.

FAQ

Do I need a Figma plugin or can I use the REST API directly?

You can use the REST API directly from Node.js with just a personal access token. No plugin required. The API is at https://api.figma.com/v1/ and the main endpoints you need are /files/:key for the document tree and /files/:key/variables/local for design tokens using the newer Variables system.

What's the difference between Figma Styles and Figma Variables for token extraction?

Styles (the older system) are named fills, text styles, and effects attached to layers. Variables (introduced in Figma 2023) are the newer token system that supports modes (light/dark) and scoping. For a proper design token pipeline that handles theming, you want Variables. The endpoint is /files/:key/variables/local. The Styles endpoint still works but doesn't give you mode variants.

My generated Tailwind classes aren't matching the Figma spacing values exactly. Why?

Tailwind's default spacing scale uses a 4px base (so gap-4 = 16px, gap-2 = 8px). If your Figma file uses values like 12px, 20px, or 28px, they fall between Tailwind's default steps. Either add those values to your Tailwind v4.0.2 @theme block as custom steps, or map them to the nearest standard value and flag the discrepancy in your generator's output log.

How do I handle components that have been manually customized after generation?

The most practical approach is a // @generated — do not edit header comment on auto-generated files, enforced by an ESLint rule (no-restricted-syntax on files matching the pattern). Keep hand-edited components in a separate directory. Run the generator only on components you haven't touched. Track which components are 'owned' by the generator in a manifest JSON file.

Can this pipeline handle responsive design from Figma?

Partially. Figma doesn't have a native responsive breakpoint system the way CSS does. Teams typically create separate frames for mobile and desktop and name them with a convention like Button/mobile and Button/desktop. Your generator can read both and emit sm: prefixed Tailwind classes for the mobile variant, but it requires that naming discipline in the design file.

Is there a rate limit on the Figma API I should know about?

Yes. The Figma REST API allows roughly 60 requests per minute for personal access tokens at the time of writing. For CI pipelines, cache the full file JSON response and use the /files/:key/versions endpoint to check whether the file has changed before re-fetching the entire document. This keeps you well under the rate limit even on active design files.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Stylelint Configuration: Catch CSS Errors Before They Reach ProdAI Component Generation in 2026: Claude, Copilot, v0 ComparedJSON Viewer in React: Collapsible Tree for API ResponsesDark Neobrutalism Cards: Bold UI for Developer Tools