·

Tiếng Việt: Real World Workflow Design To Code With Figma MCP

Real World Workflow Design To Code With Figma MCP

Năm bài trước trong module này đã lần lượt giới thiệu Figma MCP Server, rồi thử nghiệm nó với Claude Code, OpenCode, Gemini CLI và Cursor. Mỗi bài là một mảnh ghép riêng lẻ: cách cài đặt, cách OAuth, cách gọi tool. Bài capstone này ghép tất cả các mảnh đó lại thành một quy trình làm việc (workflow) hoàn chỉnh, đi từ một frame Figma do designer publish cho tới một pull request code đã sẵn sàng review.

Để bài học không sa vào lý thuyết chung chung, chúng ta sẽ bám theo đúng một case study xuyên suốt: xây dựng component PricingCard — thẻ hiển thị gói giá "Pro Plan" trên trang pricing của một sản phẩm SaaS giả định. Đây là loại component cực kỳ phổ biến trong thực tế: đủ nhỏ để làm ví dụ dễ theo, nhưng đủ phức tạp (có nested component, có token, có state) để phản ánh đúng những gì kỹ sư gặp hàng ngày. Toàn bộ prompt mẫu, code React/CSS, và lệnh git trong bài đều là những gì bạn có thể copy và chạy thử ngay, không phải giả định trừu tượng.

Lưu ý quan trọng: quy trình mô tả trong bài này không phụ thuộc vào một AI coding agent cụ thể nào. Vì Figma MCP Server là một remote MCP server chuẩn (endpoint https://mcp.figma.com/mcp, xác thực qua OAuth), nên dù bạn dùng Claude Code, Cursor, OpenCode hay Gemini CLI, bộ tool nhận được đều giống nhau: get_code, get_variable_defs, get_screenshot, get_code_connect_map, get_metadata, create_design_system_rules. Bài viết sẽ dùng Claude Code làm agent minh họa vì các ví dụ prompt gọn nhất, nhưng bạn hoàn toàn có thể chạy song song với agent khác mà không cần đổi logic.

Workflow Overview: From Figma Frame to Production-Ready UI Component

Trước khi đi vào từng bước chi tiết, cần hình dung toàn cảnh: ai làm gì, ở đâu, và bàn giao (handoff) diễn ra tại điểm nào. Một quy trình design-to-code thực tế trong team sản phẩm thường có 3 vai trò tham gia, dù không phải lúc nào cũng là 3 người khác nhau:

  1. Designer hoàn thiện frame trong Figma, đặt tên rõ ràng (ví dụ frame "Pricing Card - Pro Plan" trong file Pricing Page v2), publish lên Dev Mode — chế độ dành riêng cho việc bàn giao design sang code, nơi các thuộc tính (spacing, màu, font) được hiển thị dưới dạng có thể inspect được thay vì chỉ là hình ảnh tĩnh.
  2. Designer copy link frame (chứa node-id — định danh duy nhất của node đó trong file Figma) và dán vào ticket Jira/Linear hoặc nhắn trực tiếp cho engineer.
  3. Engineer dán link đó vào AI coding agent của mình, agent gọi Figma MCP Server để lấy design context, đối chiếu với codebase hiện có, sinh code, rồi engineer review và commit.

Điểm mấu chốt khiến quy trình này khác về chất so với việc "chụp ảnh Figma rồi tự code lại bằng tay" là: agent không chỉ nhìn thấy hình ảnh, mà lấy được dữ liệu có cấu trúc — cây node, token, và quan trọng nhất là Code Connect map, tức bản đồ cho biết phần nào của design đã có component thật tương ứng trong repo. Nhờ vậy agent biết khi nào nên tái sử dụng (Button, Badge đã tồn tại) và khi nào cần tạo mới (PricingCard chưa có).

Sơ đồ luồng cho case study PricingCard:

Designer publish frame "Pricing Card - Pro Plan" (Dev Mode)
        │
        ▼
Engineer copy link frame → dán vào AI agent (Claude Code / Cursor / OpenCode / Gemini CLI)
        │
        ▼
Bước 1: get_metadata → get_screenshot → get_code_connect_map
   (hiểu cấu trúc, có ảnh tham chiếu, biết phần nào đã có component)
        │
        ▼
Bước 2: get_variable_defs → đối chiếu tokens.css / tailwind.config.js
   (map giá trị Figma sang biến design system sẵn có, tránh hardcode)
        │
        ▼
Bước 3: get_code → sinh code PricingCard.tsx + PricingCard.css
   (import Button, Badge có sẵn) → review diff → lint/test → commit

Ba bước này không phải quy trình chỉ dùng một lần rồi bỏ (one-off). Đây là quy trình lặp lại được (repeatable): mỗi khi designer publish một frame mới, engineer chạy lại đúng chu trình này. Sau vài lần lặp, agent sẽ có sẵn rule file (từ create_design_system_rules, sẽ nói ở Bước 2) giúp các lần sau nhanh hơn hẳn vì không phải "dạy lại" agent về design system từ đầu.

Mẹo: Đặt tên frame trong Figma theo đúng tên component dự kiến trong code (ví dụ frame "PricingCard" thay vì "Pricing Card - Pro Plan (final) (v3) (dùng cái này)"). Việc này nghe nhỏ nhưng giúp agent suy luận tên file, tên component, và tên props chính xác hơn rất nhiều — vì phần lớn agent AI dùng chính tên node làm gợi ý đặt tên khi sinh code.

Step 1: Extracting Design Context và Identifying Reusable Components

Bước 1 bắt đầu khi engineer nhận được link frame, dạng như:

https://www.figma.com/design/AbCdEf123456/Pricing-Page-v2?node-id=482-1187

Phần node-id=482-1187 chính là thứ Figma MCP Server dùng để xác định chính xác node cần lấy dữ liệu — không cần mở app Figma desktop, không cần chọn tay trên canvas.

Sai lầm phổ biến nhất ở bước này: gọi thẳng get_code ngay khi có link. Với một frame nhỏ, đơn giản thì không sao, nhưng "Pricing Card - Pro Plan" trong ví dụ của chúng ta thực ra nằm lồng trong một frame lớn hơn là "Pricing Section" (chứa 3 pricing card cạnh nhau, cộng thêm toggle "Monthly/Yearly"). Nếu gọi get_code trực tiếp trên node cha, agent sẽ nhận về một khối code khổng lồ, dễ làm tràn context window (giới hạn dung lượng văn bản mà LLM có thể xử lý trong một lượt), và tệ hơn là code sinh ra thường bị "gộp" nhiều component vào một file thay vì tách đúng ranh giới.

Vì vậy thứ tự đúng là:

1. Gọi get_metadata trước để lấy cấu trúc cây node.

Prompt mẫu:

Đây là link Figma tôi cần implement: 
https://www.figma.com/design/AbCdEf123456/Pricing-Page-v2?node-id=482-1187

Hãy dùng get_metadata để lấy cấu trúc cây node của frame này trước.
Tôi muốn biết node nào là "Pricing Card - Pro Plan" cụ thể, tách biệt
với "Pricing Section" cha của nó, trước khi lấy code.

get_metadata trả về cây node dạng rút gọn (tên node, loại node, kích thước, vị trí tương đối) mà không kèm toàn bộ thuộc tính chi tiết — nhẹ hơn get_code rất nhiều lần. Nhờ đó agent (và cả bạn) xác định đúng node-id của riêng PricingCard, tránh việc lấy nhầm cả PricingSection.

2. Gọi get_screenshot để có ảnh tham chiếu trực quan.

Dữ liệu có cấu trúc (structured data) rất tốt cho việc sinh code chính xác, nhưng con người review code lại cần nhìn bằng mắt để đối chiếu. get_screenshot xuất ảnh render thực tế của node, dùng để:

  • So sánh trực quan giữa ảnh Figma và kết quả UI sau khi code chạy (visual regression thủ công).
  • Phát hiện các chi tiết mà dữ liệu text khó diễn tả hết: shadow, gradient, hiệu ứng blur ở background của card.
Prompt mẫu:

Giờ hãy dùng get_screenshot để lấy ảnh của riêng node PricingCard 
(không phải cả PricingSection). Lưu ảnh này để tôi đối chiếu sau khi 
component được code xong.

3. Gọi get_code_connect_map để biết phần nào đã có component thật.

Đây là bước quan trọng nhất và dễ bị bỏ qua nhất. Code Connect là cơ chế của Figma cho phép team liên kết (map) một component trong file Figma với component thật trong codebase (qua file cấu hình, thường đặt cạnh component, kiểu Button.figma.tsx). Khi gọi get_code_connect_map trên node PricingCard, kết quả trả về tương tự:

Node trong Figma Component đã map Trạng thái
Button / Primary src/components/Button/Button.tsx Đã có Code Connect
Badge / Popular src/components/Badge/Badge.tsx Đã có Code Connect
PricingCard (root) Chưa map, cần tạo mới

Nói cách khác: ButtonBadge bên trong card đã tồn tại trong repo, agent chỉ cần import và dùng lại, không cần sinh code mới cho chúng. Còn PricingCard với vai trò là container tổng thể thì chưa có, đây chính là phần việc thật sự của bước 3.

Prompt mẫu:

Dùng get_code_connect_map cho node PricingCard này. Với những phần đã 
có Code Connect map (ví dụ Button, Badge), khi sinh code ở bước sau, 
hãy import và dùng lại component đó thay vì viết lại từ đầu. Với 
PricingCard là component gốc, xác nhận với tôi rằng nó chưa có mapping 
trước khi mình tạo mới.

Nhờ vậy tránh được lỗi rất phổ biến khi dùng AI để code từ design: agent "vẽ lại" toàn bộ UI bằng div và style thuần, tạo ra một Button bản sao khác hẳn với Button chuẩn của design system, dẫn tới trùng lặp code và incosistency về sau.

Mẹo: Luôn gọi 3 tool theo đúng thứ tự get_metadata → get_screenshot → get_code_connect_map trước khi động tới get_code, kể cả khi frame trông "đơn giản". Chi phí gọi thêm vài tool này rất nhỏ so với chi phí phải sửa lại code bị sinh sai cấu trúc hoặc trùng lặp component.

Step 2: Mapping Figma Tokens sang biến Design System trong Code

Có được cấu trúc và biết PricingCard cần tạo mới, bước tiếp theo là lấy design token (token thiết kế — các giá trị chuẩn hoá như màu sắc, khoảng cách, bo góc, typography được đặt tên thay vì hardcode) và ánh xạ chúng sang biến tương ứng đã tồn tại trong code.

Tại sao không thể bỏ qua bước này mà để agent tự hardcode giá trị lấy trực tiếp từ Figma? Vì Figma trả về giá trị tuyệt đối, ví dụ màu nền của card là #F8FAFC, padding là 24px, border-radius là 12px. Nếu agent hardcode thẳng các con số này vào CSS, PricingCard sẽ trông đúng ngay tại thời điểm code xong — nhưng nó sẽ hoàn toàn tách rời khỏi design system. Khi team đổi theme (dark mode), đổi bảng màu, hoặc chỉnh spacing scale, PricingCard sẽ là component duy nhất "trơ" lại với giá trị cũ vì không tham chiếu biến nào cả.

1. Gọi get_variable_defs để lấy toàn bộ token áp dụng cho frame.

Prompt mẫu:

Dùng get_variable_defs cho node PricingCard, liệt kê toàn bộ token 
(màu, spacing, radius, typography) mà frame này đang dùng, kèm giá trị 
resolve cuối cùng của từng token.

Kết quả trả về dạng gần như sau:

Token Figma Giá trị resolve Dùng ở đâu trong card
color/surface/card #F8FAFC background của card
color/border/subtle #E2E8F0 border của card
color/text/primary #0F172A tiêu đề "Pro Plan"
color/brand/600 #4F46E5 giá tiền, CTA button
spacing/6 24px padding trong card
radius/lg 12px border-radius của card
typography/heading-sm 600 20px/28px Inter tiêu đề plan

2. Đối chiếu với file token hiện có trong codebase. Đây là phần việc engineer cần chủ động cung cấp context, vì agent không tự biết codebase của bạn tổ chức token ở đâu — có thể là src/styles/tokens.css (CSS variables), tailwind.config.js (nếu dùng Tailwind), hoặc src/theme/theme.ts (nếu dùng CSS-in-JS như styled-components/Emotion).

Prompt mẫu:

Trong codebase, token màu và spacing được định nghĩa tại 
src/styles/tokens.css dưới dạng CSS variables (--color-surface-card, 
--color-border-subtle, --spacing-6, --radius-lg, v.v.). Hãy đọc file 
này, rồi map từng token bạn vừa lấy từ get_variable_defs sang biến 
tương ứng đã tồn tại trong file. Nếu giá trị Figma không khớp chính 
xác với biến hiện có (lệch 1-2px), báo cho tôi biết thay vì tự ý tạo 
biến mới hoặc hardcode.

Yêu cầu "báo cho tôi biết thay vì tự ý tạo biến mới" ở đây rất quan trọng. Trong thực tế, token lấy từ Figma đôi khi lệch nhẹ so với token trong code (ví dụ do designer đã có ý định cập nhật design system nhưng code chưa theo kịp, hoặc do làm tròn số). Đây là tín hiệu cần được engineer xử lý bằng quyết định của con người — có thể là cập nhật token trong code, có thể là báo lại cho designer — chứ không nên để agent tự "chữa cháy" bằng cách sinh ra một biến CSS ăn theo, na ná (ví dụ tự thêm --radius-lg-v2), vì việc đó sẽ khiến source of truth (nguồn dữ liệu chuẩn) của design system bị phân mảnh.

3. Dùng create_design_system_rules để agent tự sinh rule file ghi nhớ mapping. Đây là tool giúp biến công sức đối chiếu ở bước trên thành tài sản dùng lại được cho các lần sau, thay vì phải lặp lại từ đầu mỗi khi có frame mới.

Prompt mẫu:

Dựa trên mapping token vừa thống nhất ở trên, hãy dùng 
create_design_system_rules để sinh ra một rule file (ví dụ 
.claude/rules/figma-design-tokens.md hoặc tương đương) ghi lại cách 
map token Figma sang CSS variable trong tokens.css, để lần sau đọc 
frame khác agent tự áp dụng lại mapping này mà không cần tôi giải 
thích lại từ đầu.

Rule file sinh ra thường liệt kê rõ quy tắc kiểu "token color/brand/600 trong Figma luôn tương ứng --color-brand-600 trong code", giúp các phiên làm việc (session) sau — kể cả với agent khác, hoặc kỹ sư khác trong team dùng chung rule file này — không phải dò lại từ đầu.

Mẹo: Sau khi có bảng mapping token, dán nó vào PR description hoặc comment đầu file CSS mới tạo. Reviewer không có ngữ cảnh Figma sẽ đọc code nhanh hơn nhiều nếu thấy ngay --color-brand-600 /* = Figma color/brand/600, dùng cho CTA button và giá */ thay vì phải tự đoán ý nghĩa từng biến.

Step 3: Generating, Refining, và Committing Component Code với AI

Đến đây agent đã có đủ ba mảnh dữ liệu: cấu trúc node, token đã map sang biến code, và biết Button/Badge là component tái sử dụng còn PricingCard là component mới. Giờ là lúc gọi get_code để sinh code thật.

1. Gọi get_code với yêu cầu rõ ràng về việc tái sử dụng component.

Prompt mẫu:

Dùng get_code cho node PricingCard. Khi sinh code React + CSS:
- Với Button và Badge: import từ src/components/Button và 
  src/components/Badge (đã có Code Connect map), không viết lại markup 
  hay style của chúng.
- Với các giá trị màu, spacing, radius, typography: dùng CSS variables 
  đã map ở bước trước (--color-surface-card, --spacing-6, --radius-lg, 
  v.v.), không hardcode giá trị số.
- Tên component: PricingCard, đặt tại src/components/PricingCard/.
- Kèm props interface hợp lý cho planName, price, features (list), 
  isPopular (boolean) để card có thể tái dùng cho cả 3 gói giá, không 
  chỉ hardcode riêng cho "Pro Plan".

Kết quả sinh ra (rút gọn để minh hoạ) thường trông như sau:

// src/components/PricingCard/PricingCard.tsx
import { Button } from "../Button/Button";
import { Badge } from "../Badge/Badge";
import "./PricingCard.css";

export interface PricingCardProps {
  planName: string;
  price: string;
  billingCycle: "monthly" | "yearly";
  features: string[];
  isPopular?: boolean;
  onSelect: () => void;
}

export function PricingCard({
  planName,
  price,
  billingCycle,
  features,
  isPopular = false,
  onSelect,
}: PricingCardProps) {
  return (
    <div className={`pricing-card${isPopular ? " pricing-card--popular" : ""}`}>
      {isPopular && (
        <Badge variant="brand" aria-label="Gói được chọn nhiều nhất">
          Phổ biến nhất
        </Badge>
      )}
      <h3 className="pricing-card__plan-name">{planName}</h3>
      <p className="pricing-card__price">
        <span className="pricing-card__price-value">{price}</span>
        <span className="pricing-card__price-cycle">
          /{billingCycle === "monthly" ? "tháng" : "năm"}
        </span>
      </p>
      <ul className="pricing-card__features">
        {features.map((feature) => (
          <li key={feature}>{feature}</li>
        ))}
      </ul>
      <Button
        variant="primary"
        onClick={onSelect}
        aria-label={`Chọn gói ${planName}`}
      >
        Bắt đầu ngay
      </Button>
    </div>
  );
}
/* src/components/PricingCard/PricingCard.css */
.pricing-card {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-4);
  padding: var(--spacing-6);
  background: var(--color-surface-card);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
}

.pricing-card--popular {
  border-color: var(--color-brand-600);
  box-shadow: 0 0 0 1px var(--color-brand-600);
}

.pricing-card__plan-name {
  font: var(--typography-heading-sm);
  color: var(--color-text-primary);
}

.pricing-card__price-value {
  font-size: 2rem;
  font-weight: 700;
  color: var(--color-brand-600);
}

2. Review diff trước khi tin bất kỳ dòng nào. Đây là bước mà nhiều engineer mới dùng AI coding agent hay bỏ qua vì "trông đã đúng như ảnh Figma rồi". Nhưng ảnh screenshot tĩnh không thể hiện được:

  • Accessibility: alt text cho icon nếu có, aria-label cho nút bấm — như ví dụ trên đã thêm aria-label cho BadgeButton, nhưng agent không phải lúc nào cũng tự làm việc này nếu không được nhắc, cần kiểm tra kỹ.
  • Responsive behavior: Figma frame là một kích thước cố định (ví dụ 360px width), nhưng thực tế card phải chạy tốt trong grid 3 cột trên desktop và full-width trên mobile. Cần tự tay test resize, không có tool MCP nào làm thay được việc này.
  • State ngoài thiết kế: trạng thái loading, disabled của nút "Bắt đầu ngay" khi gọi API thường không có trong design gốc, engineer phải bổ sung logic riêng.
npm run lint -- src/components/PricingCard
npm run test -- PricingCard

3. Commit theo convention của team.

git add src/components/PricingCard/
git diff --staged
git commit -m "feat: add PricingCard component from Figma design"

Nếu team có quy ước gắn số ticket hoặc link Figma vào commit message, nên thêm vào để người review sau này (kể cả chính bạn 6 tháng sau) truy ngược lại đúng frame gốc:

git commit -m "feat: add PricingCard component from Figma design

Source: https://www.figma.com/design/AbCdEf123456/Pricing-Page-v2?node-id=482-1187
Reuses: Button, Badge (Code Connect mapped)
Tokens mapped via get_variable_defs, see src/styles/tokens.css"

Từ đây, PricingCard trở thành một component có Code Connect map hoàn chỉnh cho lần sau — nếu designer sửa lại "Pro Plan" thành "Business Plan" và publish frame mới, vòng lặp Bước 1 → 2 → 3 chạy lại gần như tự động, nhanh hơn hẳn lần đầu vì rule file và Code Connect map đã tồn tại sẵn.

Mẹo: Đừng merge code sinh từ AI chỉ vì nó "chạy được và giống ảnh Figma". Luôn tự hỏi 3 câu: component có tái sử dụng đúng những gì đã có (Button, Badge) không, token có dùng biến thay vì hardcode không, và accessibility/responsive có được xử lý dù không thấy trong ảnh tĩnh không. Ba câu hỏi này chính là ranh giới giữa "code chạy được" và "code sẵn sàng production".