There's a moment every frontend developer knows well: the designer drops a link to a Figma file in Slack, and suddenly you're staring at a pixel-perfect composition wondering how to turn those beautifully arranged rectangles into maintainable, accessible, production-grade React code.
It's not as simple as copying colors and font sizes. A Figma file is a static snapshot of an intent — and your job is to breathe life into it. This guide walks you through a battle-tested, professional workflow to go from Figma to React with clean architecture, accessibility baked in, and components built to last.
Whether you're a junior developer learning the ropes or an intermediate engineer looking to sharpen your process, this guide is for you.
Why the Figma → React Gap Exists
Figma and React live in fundamentally different worlds. Figma thinks in layers, groups, and artboards. React thinks in components, state, and props. Designers think in visual hierarchy and spacing. Developers think in DOM structure, event bubbling, and render cycles.
This conceptual mismatch creates common failure modes:
- Deeply nested
<div>soups with no semantic meaning - Hardcoded pixel values that break at different screen sizes
- Components that are impossible to reuse because they're too tightly coupled to one context
- Accessibility overlooked because the design "looks fine"
- Design tokens ignored — colors and typography duplicated everywhere
A structured workflow fixes all of this before you write a single line of JSX.
Step 1: Understand the Design Before You Code
Before you open your editor, spend real time in Figma. This phase is not optional.
Audit the Design for Structure
Look at the Figma file and ask:
- What are the repeating visual patterns? (cards, list items, buttons)
- What variants exist? (button states: default, hover, disabled, loading)
- What breakpoints has the designer considered? (desktop, tablet, mobile frames)
- Where does content change vs. where does structure stay constant?
This audit directly maps to your component tree.
Inspect Design Tokens
Modern Figma files use Variables (Figma's native design token system) or third-party plugins like Tokens Studio. Before you touch code, identify:
- Colors — primary, secondary, neutral, semantic (success, error, warning)
- Typography — font families, sizes, weights, line heights
- Spacing — the spacing scale the designer used (4px base? 8px?)
- Border radii, shadows, z-index layers
These become your CSS custom properties or your design token file in code.
Communicate With the Designer
Flag ambiguities early. Ask:
- "What happens to this card when the title is 80 characters long?"
- "Is this button disabled state permanent or conditional on form validation?"
- "What's the focus state for keyboard users on this dropdown?"
Designers who haven't explicitly thought about these cases will appreciate the question. You'll save hours of back-and-forth later.
Step 2: Set Up Your Design Token Foundation
The biggest long-term mistake developers make is hardcoding design values. Instead, translate Figma variables into code-level tokens immediately.
Creating a Token File
// tokens/index.ts
export const tokens = {
color: {
brand: {
primary: "#2563EB",
primaryHover: "#1D4ED8",
primaryLight: "#DBEAFE",
},
neutral: {
50: "#F8FAFC",
100: "#F1F5F9",
200: "#E2E8F0",
700: "#334155",
900: "#0F172A",
},
semantic: {
success: "#16A34A",
error: "#DC2626",
warning: "#D97706",
},
},
spacing: {
1: "4px",
2: "8px",
3: "12px",
4: "16px",
6: "24px",
8: "32px",
12: "48px",
16: "64px",
},
typography: {
fontFamily: {
sans: "'Inter Variable', system-ui, sans-serif",
mono: "'JetBrains Mono', monospace",
},
fontSize: {
sm: "0.875rem",
base: "1rem",
lg: "1.125rem",
xl: "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
},
borderRadius: {
sm: "4px",
md: "8px",
lg: "12px",
full: "9999px",
},
} as const;
CSS Custom Properties
Map your tokens to CSS variables for use in stylesheets:
/* styles/tokens.css */
:root {
/* Colors */
--color-brand-primary: #2563eb;
--color-brand-primary-hover: #1d4ed8;
--color-brand-primary-light: #dbeafe;
--color-neutral-50: #f8fafc;
--color-neutral-900: #0f172a;
--color-error: #dc2626;
--color-success: #16a34a;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
/* Typography */
--font-sans: "Inter Variable", system-ui, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-2xl: 1.5rem;
/* Radius */
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
}
If you use Tailwind CSS, extend the config with these tokens instead:
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
brand: {
primary: "var(--color-brand-primary)",
"primary-hover": "var(--color-brand-primary-hover)",
},
},
fontFamily: {
sans: ["Inter Variable", "system-ui", "sans-serif"],
},
},
},
};
Step 3: Decompose the Design Into a Component Tree
This is where design thinking becomes engineering thinking. Look at your Figma screens and break them down systematically.
The Atomic Design Mental Model
Use Brad Frost's Atomic Design as a mental framework:
- Atoms: Buttons, inputs, icons, labels, badges
- Molecules: Form field (label + input + error message), Card header (avatar + title + timestamp)
- Organisms: Navigation bar, Product card grid, Comment thread
- Templates: Page layouts (sidebar + main content)
- Pages: Specific instances of templates with real content
Practical Example: A Product Card
Imagine a Figma design for an e-commerce product card. Decompose it:
ProductCard (organism)
├── CardImage (atom) — product photo with lazy loading
├── CardBadge (atom) — "New" or "Sale" tag
├── CardBody (molecule)
│ ├── ProductTitle (atom) — <h3> with proper heading level
│ ├── ProductRating (molecule) — stars + review count
│ └── PriceDisplay (molecule) — original price + sale price
└── CardActions (molecule)
├── AddToCartButton (atom)
└── WishlistButton (atom)
This decomposition happens before you write any JSX. It gives you a component checklist and prevents you from writing a 400-line monolithic component.
Step 4: Write Semantic HTML First
One of the most common and costly mistakes is reaching for <div> and <span> reflexively. Semantic HTML gives your UI meaning — for screen readers, search engines, and your future teammates.
Semantic Element Cheat Sheet
| Visual Element | Correct Semantic HTML |
|---|---|
| Page header with logo/nav | <header> |
| Site navigation | <nav> |
| Main page content | <main> |
| Sidebar content | <aside> |
| Article or blog post | <article> |
| Grouped related content | <section> |
| Page footer | <footer> |
| Clickable button | <button> |
| Navigation link | <a href="..."> |
| Form label | <label> |
| Data table | <table> with <thead>, <tbody> |
| Image with meaning | <img alt="Descriptive text"> |
| Decorative image | <img alt=""> (empty alt) |
Building the Product Card With Semantic HTML
// components/ProductCard/ProductCard.tsx
import type { FC } from "react";
import styles from "./ProductCard.module.css";
interface ProductCardProps {
id: string;
title: string;
imageUrl: string;
imageAlt: string;
price: number;
originalPrice?: number;
rating: number;
reviewCount: number;
badge?: "new" | "sale";
onAddToCart: (id: string) => void;
onWishlist: (id: string) => void;
}
export const ProductCard: FC<ProductCardProps> = ({
id,
title,
imageUrl,
imageAlt,
price,
originalPrice,
rating,
reviewCount,
badge,
onAddToCart,
onWishlist,
}) => {
const isOnSale = originalPrice !== undefined && originalPrice > price;
const discount = isOnSale
? Math.round(((originalPrice - price) / originalPrice) * 100)
: 0;
return (
<article className={styles.card} aria-label={title}>
<div className={styles.imageWrapper}>
<img
src={imageUrl}
alt={imageAlt}
className={styles.image}
loading="lazy"
width={320}
height={240}
/>
{badge && (
<span
className={`${styles.badge} ${styles[`badge--${badge}`]}`}
aria-label={badge === "sale" ? `${discount}% off` : "New arrival"}
>
{badge === "sale" ? `-${discount}%` : "New"}
</span>
)}
</div>
<div className={styles.body}>
<h3 className={styles.title}>{title}</h3>
{/* Rating */}
<div className={styles.rating} aria-label={`Rated ${rating} out of 5`}>
<StarRating value={rating} />
<span className={styles.reviewCount}>
({reviewCount.toLocaleString()})
</span>
</div>
{/* Price */}
<div className={styles.priceGroup}>
<span className={styles.price} aria-label={`Current price $${price}`}>
${price.toFixed(2)}
</span>
{isOnSale && (
<del className={styles.originalPrice} aria-label={`Original price $${originalPrice}`}>
${originalPrice!.toFixed(2)}
</del>
)}
</div>
</div>
<div className={styles.actions}>
<button
className={styles.addToCart}
onClick={() => onAddToCart(id)}
type="button"
aria-label={`Add ${title} to cart`}
>
Add to Cart
</button>
<button
className={styles.wishlist}
onClick={() => onWishlist(id)}
type="button"
aria-label={`Save ${title} to wishlist`}
aria-pressed={false}
>
<HeartIcon aria-hidden="true" />
</button>
</div>
</article>
);
};
Notice several deliberate choices here:
<article>wraps the card — it's a self-contained unit of content<h3>for the title — heading level determined by page hierarchy, not font size<del>for the strikethrough original price — semantically means "deleted" contentaria-labelon the rating<div>so screen readers announce it meaningfullyaria-pressedon the wishlist button — announces toggle statearia-hidden="true"on the decorative icon- Explicit
type="button"to prevent accidental form submission
Step 5: Build Accessible, Reusable Atoms
Your atom-level components become the building blocks of everything. Invest in them properly.
A Robust Button Component
// components/Button/Button.tsx
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
import { clsx } from "clsx";
import styles from "./Button.module.css";
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
children: ReactNode;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "primary",
size = "md",
isLoading = false,
leftIcon,
rightIcon,
children,
disabled,
className,
...rest
},
ref
) => {
const isDisabled = disabled || isLoading;
return (
<button
ref={ref}
className={clsx(
styles.button,
styles[`button--${variant}`],
styles[`button--${size}`],
isLoading && styles["button--loading"],
className
)}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-busy={isLoading}
{...rest}
>
{isLoading && (
<span className={styles.spinner} aria-hidden="true" />
)}
{!isLoading && leftIcon && (
<span className={styles.icon} aria-hidden="true">
{leftIcon}
</span>
)}
<span className={isLoading ? styles.loadingText : undefined}>
{children}
</span>
{!isLoading && rightIcon && (
<span className={styles.icon} aria-hidden="true">
{rightIcon}
</span>
)}
</button>
);
}
);
Button.displayName = "Button";
Key patterns here:
forwardRef— allows parent components to access the DOM node (important for focus management)aria-busy— announces loading state to assistive technologiesaria-disabledvsdisabled— using both ensures the element is announced as disabled even when focus is kept on it for UX reasonsdisplayName— essential for React DevTools debugging- Spreading
...rest— allows any native button attribute to be passed through
A Fully Accessible Text Input
// components/Input/Input.tsx
import { forwardRef, useId, type InputHTMLAttributes } from "react";
import { clsx } from "clsx";
import styles from "./Input.module.css";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
helperText?: string;
errorMessage?: string;
isRequired?: boolean;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
label,
helperText,
errorMessage,
isRequired,
id: externalId,
className,
...rest
},
ref
) => {
const generatedId = useId();
const id = externalId ?? generatedId;
const helperId = `${id}-helper`;
const errorId = `${id}-error`;
const hasError = Boolean(errorMessage);
return (
<div className={styles.fieldWrapper}>
<label htmlFor={id} className={styles.label}>
{label}
{isRequired && (
<span aria-hidden="true" className={styles.required}>
*
</span>
)}
</label>
<input
ref={ref}
id={id}
className={clsx(
styles.input,
hasError && styles["input--error"],
className
)}
aria-required={isRequired}
aria-invalid={hasError}
aria-describedby={
[helperText && helperId, hasError && errorId]
.filter(Boolean)
.join(" ") || undefined
}
{...rest}
/>
{helperText && !hasError && (
<p id={helperId} className={styles.helperText}>
{helperText}
</p>
)}
{hasError && (
<p id={errorId} className={styles.errorMessage} role="alert">
{errorMessage}
</p>
)}
</div>
);
}
);
Input.displayName = "Input";
This handles a surprisingly tricky accessibility problem: aria-describedby must reference the IDs of elements that provide supplementary information. The error message gets role="alert" so screen readers announce it immediately when it appears.
Step 6: Handle Responsive Layout
Figma designs often come in two or three breakpoint frames. Your job is to express those breakpoints in code without magic numbers.
Responsive Breakpoint Tokens
/* styles/breakpoints.css */
:root {
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
}
CSS Grid for Adaptive Layouts
Prefer auto-fit / auto-fill grid patterns over explicit media queries wherever possible:
/* A card grid that adapts without breakpoints */
.productGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-6);
padding: var(--space-6);
}
This single rule produces 1 column on mobile, 2 on tablet, 3–4 on desktop — all without a single media query.
When You Do Need Media Queries
.heroSection {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-8);
align-items: center;
}
@media (min-width: 1024px) {
.heroSection {
grid-template-columns: 1fr 1fr;
}
}
Always write mobile-first: start with the smallest layout, add complexity at larger sizes.
Step 7: Component Composition Over Configuration
A common trap is the "mega-component" — a single component with 30 props that handles every possible variation. Instead, use composition.
Avoid Prop Explosion
// ❌ Don't do this — prop explosion
<Card
hasImage
imagePosition="top"
hasTitle
titleSize="lg"
hasSubtitle
hasActions
actionAlignment="right"
hasBadge
badgeVariant="success"
/>
// ✅ Do this — composition
<Card>
<Card.Image src={imageUrl} alt={imageAlt} />
<Card.Body>
<Card.Title size="lg">{title}</Card.Title>
<Card.Subtitle>{subtitle}</Card.Subtitle>
</Card.Body>
<Card.Footer align="right">
<Badge variant="success">In Stock</Badge>
<Button variant="primary">Buy Now</Button>
</Card.Footer>
</Card>
Implementing Compound Components
// components/Card/Card.tsx
import {
createContext,
useContext,
type FC,
type ReactNode,
type HTMLAttributes,
} from "react";
import styles from "./Card.module.css";
const CardContext = createContext<{ elevated?: boolean }>({});
interface CardProps extends HTMLAttributes<HTMLElement> {
children: ReactNode;
elevated?: boolean;
as?: keyof JSX.IntrinsicElements;
}
const CardRoot: FC<CardProps> = ({
children,
elevated = false,
as: Element = "div",
className,
...rest
}) => (
<CardContext.Provider value={{ elevated }}>
<Element
className={`${styles.card} ${elevated ? styles["card--elevated"] : ""} ${className ?? ""}`}
{...rest}
>
{children}
</Element>
</CardContext.Provider>
);
const CardImage: FC<{ src: string; alt: string; className?: string }> = ({
src,
alt,
className,
}) => (
<div className={`${styles.imageWrapper} ${className ?? ""}`}>
<img src={src} alt={alt} className={styles.image} loading="lazy" />
</div>
);
const CardBody: FC<{ children: ReactNode; className?: string }> = ({
children,
className,
}) => (
<div className={`${styles.body} ${className ?? ""}`}>{children}</div>
);
const CardTitle: FC<{
children: ReactNode;
as?: "h2" | "h3" | "h4";
className?: string;
}> = ({ children, as: Element = "h3", className }) => (
<Element className={`${styles.title} ${className ?? ""}`}>{children}</Element>
);
const CardFooter: FC<{
children: ReactNode;
align?: "left" | "right" | "center";
className?: string;
}> = ({ children, align = "left", className }) => (
<div
className={`${styles.footer} ${styles[`footer--${align}`]} ${className ?? ""}`}
>
{children}
</div>
);
// Attach sub-components as properties
export const Card = Object.assign(CardRoot, {
Image: CardImage,
Body: CardBody,
Title: CardTitle,
Footer: CardFooter,
});
Step 8: Test What Matters
Visual Regression Testing
Use Storybook with Chromatic or Playwright's screenshot testing to catch visual regressions when components change.
Accessibility Testing
Run these tools as part of your development workflow:
- axe DevTools browser extension — real-time a11y violations
- @axe-core/react — in-component testing during development
- Playwright + axe — automated a11y checks in CI
// Example: accessibility test with Playwright
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("ProductCard has no accessibility violations", async ({ page }) => {
await page.goto("/storybook/product-card--default");
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Keyboard Navigation Testing
Manually test every component with keyboard only:
- Tab to focus the component
- Use arrow keys, Enter, Escape as appropriate
- Verify focus indicators are visible (never remove
outlinewithout replacing it) - Verify focus is trapped inside modals and dialogs
🚀 Pro Tips
Extract spacing from Figma mathematically. If a designer used an 8px grid, every spacing value will be a multiple of 8. Confirm the base unit and codify it in your token scale.
Use Figma's "Inspect" panel for exact values, but verify intent. Sometimes a 13px value is a mistake — the designer meant 12px or 14px. Ask before hardcoding oddities.
Component names should match Figma layer names. When the designer has named a layer ProductCard/Default, your component should be ProductCard. This creates a shared vocabulary and eliminates translation overhead in design reviews.
Don't skip the displayName. In production builds, minification can strip component names. Component.displayName = "MyComponent" preserves it in React DevTools and error stack traces.
Build dark mode from day one with CSS custom properties. Retrofitting dark mode into an existing codebase is painful. Start with CSS variables from your token layer and add a [data-theme="dark"] selector from the beginning.
[data-theme="dark"] {
--color-brand-primary: #60a5fa;
--color-neutral-900: #f8fafc;
--color-neutral-50: #0f172a;
}
Use logical CSS properties for internationalization. Instead of margin-left and padding-right, use margin-inline-start and padding-inline-end. This automatically handles RTL layouts for Arabic and Hebrew without extra code.
Common Mistakes to Avoid
1. Using <div onClick> Instead of <button>
A <div> with a click handler is invisible to keyboard users and screen readers. Always use <button> for interactive controls, or <a href> for navigation. If you must use a <div> for some reason, add role="button", tabIndex={0}, and handle both onClick and onKeyDown.
2. Pixel-Perfect Obsession
Pixel-perfect implementation is not the goal — behavior-perfect is. A design that looks exactly right at 1440px wide but breaks at 1380px is a worse outcome than a design that deviates by 2 pixels but works beautifully at all sizes.
3. Ignoring the Interactive States
Figma designs often only show the default state. Always account for: hover, focus, active, disabled, loading, empty, error, and success states. If the designer hasn't specified them, define them yourself using the established visual language.
4. Skipping alt Text
Every <img> needs an alt attribute. If the image is decorative (a background texture, an icon repeated elsewhere in text), use alt="". If the image carries meaning, write a description that conveys that meaning — not "a photo of" but "Customer Sarah M. smiling in her kitchen."
5. Creating Components Too Early
Don't abstract into a reusable component until you have at least two concrete instances. Premature abstraction creates overly complex APIs for problems that only needed a simple className prop.
6. Hardcoding Colors and Fonts
The single fastest way to make a codebase unmaintainable is scattering #2563EB and font-family: "Inter" across 200 files. Every visual value goes through the token system.
📌 Key Takeaways
- Audit before you code. Spend real time in Figma understanding structure, tokens, and variants before opening your editor.
- Design tokens are non-negotiable. Every color, spacing value, and font decision belongs in a token file, not hardcoded inline.
- Semantic HTML is accessibility. Using the right HTML element correctly is the highest-ROI accessibility investment you can make.
- Decompose before you build. A component tree diagram on paper prevents 400-line monolith components in code.
- Compose, don't configure. Compound component patterns scale better than props explosion.
- Interactive states are part of the design. Hover, focus, disabled, loading, error — all of them need implementation.
- Test with keyboard and a screen reader. Real users depend on them. You should experience your UI the same way.
- Pixel-perfect is not the goal. Behavior-perfect, accessible, and maintainable are the goals.
Conclusion
Going from Figma to React is a craft, not a mechanical translation. The developers who do it well aren't just reading colors off an inspect panel — they're thinking about component boundaries, accessibility trees, design token architecture, and responsive behavior simultaneously.
The workflow in this guide — audit, tokenize, decompose, write semantic HTML, build accessible atoms, compose, and test — gives you a repeatable, professional process that produces code your team will actually want to maintain.
The next time a Figma link drops in your Slack, you'll know exactly where to start.
References
- Figma Variables Documentation — Official guide to Figma's design token system
- MDN: HTML Elements Reference — Semantic HTML element documentation
- W3C ARIA Authoring Practices Guide — Patterns for building accessible components
- Atomic Design by Brad Frost — The mental model for component hierarchy
- React: forwardRef — Official React docs on ref forwarding
- CSS Logical Properties — Writing layout-agnostic, internationalization-friendly CSS
- axe-core — Open-source accessibility testing engine
- Inclusive Components by Heydon Pickering — Deep dives into accessible UI patterns