My Figma to Next.js Component Workflow

Photo by Unsplash

Photo by Unsplash
There's a painful gap between a polished Figma design and a working React component. Pixel values that don't map to Tailwind defaults. Color names in the design system that bear no relation to the CSS variable names in the codebase. After iterating on this workflow across several projects — including this portfolio — I've landed on a process that keeps design fidelity high and translation time low.
The single biggest time-saver is spending 30 minutes auditing the Figma file before touching code. What I'm looking for: inconsistencies in the design token usage, auto-layout usage (Figma's auto-layout maps directly to CSS flexbox), and component hierarchy (are variants used consistently?). A design that uses auto-layout throughout and a proper local variables/styles setup translates almost mechanically.
Figma's Dev Mode (the inspector panel with the less-than greater-than toggle) is useful but requires interpretation. It gives you exact pixel values, auto-layout direction and gap, corner radius, opacity, and color in various formats. What it doesn't give you is semantic meaning — it won't tell you that a color is brand-500 in your design system, or that a spacing value maps to gap-6 in Tailwind.
The Figma variables panel and the Tailwind config need to use the same names for the same values. I set up the Tailwind config before writing components, extending the theme to match Figma variable names exactly: if Figma has color/brand/500, the Tailwind key is brand-500. This creates a one-to-one mapping: when a designer says 'use brand-500', I know exactly which Tailwind class to write.
┌─────────────┐ Inspect ┌──────────────────────┐
│ FIGMA │ ─────────────► │ Design Tokens │
│ (Design) │ Dev Mode │ colors, spacing, │
└─────────────┘ │ typography, radius │
└──────────┬───────────┘
│ tailwind.config.ts
▼
┌─────────────────────────────────────────────────────┐
│ COMPONENT MAPPING │
│ │
│ Figma Frame/Group → React Component │
│ Figma Auto-layout → flex / grid classes │
│ Figma Variant → TypeScript props union │
│ Figma Component → Storybook story │
└──────────────────────────┬──────────────────────────┘
│ build
▼
Next.js Page
(server components)Install the Figma plugin Tokens Studio for Figma (formerly Style Dictionary) to export design tokens directly as a JSON file, then use Style Dictionary to generate your Tailwind config extension automatically. For projects with more than 50 unique tokens, this eliminates an entire category of manual sync errors.
Every Figma component becomes a TypeScript interface. Figma variants map to TypeScript union types. Figma boolean properties map to optional boolean props. The challenge is deciding what to expose as a prop versus what to hardcode. My rule: anything the designer explicitly made a variant or a configurable style override should be a prop.
Figma designs are typically for a single viewport width — usually desktop (1440px) or mobile (375px). Responsive behavior is implied, not always explicitly designed. When translating to Next.js, I use Tailwind's responsive prefixes (sm:, md:, lg:) to interpolate between the two designed states.
// tailwind.config.ts — mirror Figma token names exactly
import type { Config } from "tailwindcss"
const config: Config = {
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6", // match Figma "brand/500"
900: "#1e3a5f",
},
},
spacing: {
18: "4.5rem", // custom Figma spacing token
},
borderRadius: {
"2xl": "1rem", // Figma corner radius
},
},
},
}
export default config
// components/ui/Button.tsx — typed props from Figma variants
type ButtonVariant = "primary" | "secondary" | "ghost"
type ButtonSize = "sm" | "md" | "lg"
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
loading?: boolean
}
const variantClasses: Record<ButtonVariant, string> = {
primary: "bg-brand-500 text-white hover:bg-brand-600",
secondary: "bg-transparent border border-brand-500 text-brand-500",
ghost: "bg-transparent text-brand-500 hover:bg-brand-50",
}
const sizeClasses: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
}
export function Button({
variant = "primary",
size = "md",
loading,
children,
className,
...props
}: ButtonProps) {
return (
<button
className={`rounded-2xl font-medium transition-colors ${variantClasses[variant]} ${sizeClasses[size]} ${className ?? ""}`}
disabled={loading}
{...props}
>
{loading ? <span className="animate-spin">⏳</span> : children}
</button>
)
}Every component I build from Figma gets a Storybook story alongside it. The story covers each variant exactly as it appears in Figma, plus edge cases the design doesn't show — empty states, long strings, loading states, error states. Storybook serves as a living bridge between the design and the implementation.
Spending hours matching a 2px spacing difference visible only at 200% zoom is almost always the wrong tradeoff. Ship at 95% fidelity quickly, collect user feedback, then refine. Agree on an explicit tolerance level with your designer before the component review — it saves arguments and late-night pixel-hunting sessions.
Icon management is a detail that causes disproportionate friction. I use Lucide React as the default icon library — it's tree-shakeable, consistent in style, and covers the vast majority of UI needs. For custom icons from the design (the brand logo, product-specific icons), I export them from Figma as SVGs, optimize them with SVGO, and wrap them in a simple React component that accepts a size prop and className.
Sometimes I'm building features where the designer is still working on the Figma file, or there's no designer at all. In that case, I start with the component API first — define the TypeScript interface for the props, stub the component with placeholder content, and write the Storybook story. The component design emerges from the props, not the other way around.