Skip to main content
Back to Blog
ReactFigmaFrontendUI DevelopmentAccessibilityComponent DesignDesign SystemsTypeScriptCSSWeb Development

From Figma to React: How to Translate Designs into Clean Code

A practical, end-to-end workflow for converting Figma designs into production-ready React components — covering accessibility, semantic HTML, design tokens, and reusable component architecture.

April 29, 202616 min readNiraj Kumar

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 ElementCorrect 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" content
  • aria-label on the rating <div> so screen readers announce it meaningfully
  • aria-pressed on the wishlist button — announces toggle state
  • aria-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 technologies
  • aria-disabled vs disabled — using both ensures the element is announced as disabled even when focus is kept on it for UX reasons
  • displayName — 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:

  1. Tab to focus the component
  2. Use arrow keys, Enter, Escape as appropriate
  3. Verify focus indicators are visible (never remove outline without replacing it)
  4. 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

All Articles
ReactFigmaFrontendUI DevelopmentAccessibilityComponent DesignDesign SystemsTypeScriptCSSWeb Development

Written by

Niraj Kumar

Software Developer — building scalable systems for businesses.