Tooling & Quy trình Design-to-DevTooling & Design-to-Dev Workflow
Thuộc bộ kiến thức Design System Roadmap.
Tổng quan
Mỗi lần hand-off giữa designer và engineer là một cơ hội để mọi thứ trôi dạt (drift) khỏi nhau. Không có vocabulary chung, designer vẽ một button với padding 12px và màu fill #3B82F6; engineer lại build nó với 10px và #3B72F5 vì đó là giá trị dùng ở ticket trước. Nhân con số đó lên hàng trăm màn hình và hàng chục engineer, sản phẩm sẽ dần rã ra thành hàng nghìn component gần-giống-nhau nhưng sai lệch chút ít. Đây không phải vấn đề về kỷ luật làm việc — mà là vấn đề về tooling. Không ai có thể nhớ chính xác “thang spacing đúng” trong đầu khi làm việc với hàng trăm file.
Design system giải quyết việc này bằng cách biến bản thân hệ thống thành nguồn chân lý (source of truth), và tooling chính là thứ giữ cho design và code luôn trỏ về cùng một nguồn đó thay vì trôi dạt thành hai thực tại song song. Bài này bao quát toolchain cụ thể giúp điều đó khả thi: design editor (gần như luôn là Figma ở thời điểm hiện tại) và các plugin biến nó từ công cụ vẽ thành công cụ nhận biết token; các định dạng interchange (chủ yếu là JSON) giúp token chảy qua lại giữa design và code; UI kit phản chiếu chính xác thư viện component đã code trong Figma; Storybook như workbench nơi component thực sự được build và document bằng code; Style Dictionary như pipeline biến một nguồn token duy nhất thành định dạng gốc của mọi nền tảng; và quyết định về tech stack (framework-specific hay framework-agnostic) — quyết định một thư viện component code có thể vươn xa tới đâu trong tổ chức.
Không công cụ nào trong số này là design system — chúng là hệ thống ống nước (plumbing) giúp design system trở thành thứ vận hành thực sự thay vì một file PDF hướng dẫn tĩnh mà chẳng ai tuân theo.
Kiến thức nền tảng
Vấn đề hand-off, nói chính xác
“Hand-off design-to-dev” theo cách truyền thống nghĩa là: designer hoàn thành một file Figma, engineer mở file đó lên, và engineer đọc thủ công các giá trị — màu sắc, spacing, font size, corner radius — rồi tự tay mã hóa lại chúng thành CSS hoặc một object StyleSheet. Mỗi lần đọc như vậy là một cơ hội để xảy ra sai số làm tròn, click nhầm, hoặc một xấp xỉ “gần đúng là được”. Tệ hơn, hai artifact (file Figma và codebase) không có liên kết cấu trúc nào với nhau: nếu designer sprint sau chỉnh lại một token spacing, không có gì báo cho engineer biết component của họ giờ đã lỗi thời.
Một toolchain trưởng thành cho design system sẽ loại bỏ càng nhiều bước mã hóa lại thủ công này càng tốt:
| Bước hand-off thủ công | Thay thế bằng tooling |
|---|---|
| Designer chọn màu bằng mắt, engineer đọc mã hex bằng ước lượng từ inspector của Figma | Cả hai cùng tham chiếu một token đặt tên (color.brand.primary) xuất ra từ một nguồn duy nhất |
| Engineer đoán spacing bằng thước đo | Token spacing (space.4 = 16px) được chia sẻ qua output của Style Dictionary |
| Designer vẽ lại button từ đầu cho một màn hình mới | Designer kéo một instance của component Button từ thư viện Figma chung, với cùng các variant như component đã code |
| Engineer build component, hy vọng nó khớp ý đồ thiết kế | Engineer build dựa trên Storybook, designer review chính instance Storybook đã deploy thay vì một mock tĩnh |
| Hỏi trên Slack “component này đã có ai làm chưa?” | Analytics về usage của component/token cho thấy cái gì đã tồn tại (xem Đo lường thành công: Analytics & Testing) |
Vị trí của từng công cụ trong pipeline
Sẽ dễ hình dung hơn nếu coi toolchain là một pipeline với một nguồn chân lý duy nhất chảy theo hai hướng — vào design tool và vào codebase:
- Token sourceJSON / Tokens Studio / DTCG
- Figma (design)via Tokens Studio plugin, syncs to Figma VariablesStyle Dictionarybuild pipeline
- Figma UI Kitmirrors coded component APICSS custom propertiesiOS (Swift/.plist)Android (XML/Kotlin)
- designer composes screensProduct design files
- Coded component libraryReact / Vue / Web Components
- developed & documented inStorybook
Hai nửa — design và code — không bao giờ nói chuyện trực tiếp với nhau. Cả hai cùng tiêu thụ (consume) một nguồn token, và lý tưởng là cùng một đặc tả (specification) component, giữ cho mọi thứ trung thực mà không cần một API sống nối giữa Figma và repo của bạn.
Khái niệm chính
Figma — design editor thống trị
Figma đã trở thành design tool mặc định cho các team xây dựng design system, vì những lý do đặc biệt quan trọng với công việc làm hệ thống chứ không chỉ thiết kế UI thông thường:
- Component và variant. Một component Figma có thể có nhiều variant (size:
sm/md/lg, state:default/hover/disabled, intent:primary/secondary) gộp lại thành một component set với một panel thuộc tính — điều này ánh xạ gần như một-một vào prop API của component đã code (<Button size="lg" variant="primary" />). Designer chọn variant trên panel bên phải của Figma đang thực hiện đúng thao tác tư duy giống engineer khi set props. - Auto-layout. Auto-layout của Figma (sizing kiểu flex, padding, gap, hành vi resize) mô phỏng CSS flexbox/grid đủ gần để một frame auto-layout được build tốt có thể chuyển gần như trực tiếp thành layout CSS, thay vì engineer phải reverse-engineer vị trí pixel từ một frame tĩnh.
- Figma Variables. Hỗ trợ native cho biến có kiểu (color, number, string, boolean) với khả năng chuyển mode (ví dụ: light/dark, density) ánh xạ trực tiếp vào design token — bao gồm cả aliasing (một semantic token tham chiếu tới một core token) và thư viện xuyên file.
- Dev Mode. Một chế độ inspect chuyên dành cho engineer, hiển thị tên variable/token bên dưới, spacing, và (qua plugin) sinh ra code snippet, thay vì buộc engineer phải dùng chung canvas mà designer dùng để vẽ.
Không có gì trong số này là độc quyền mãi mãi của Figma, nhưng ở thời điểm hiện tại, đây là công cụ mà hầu hết tài liệu tham khảo về design system (kể cả roadmap.sh) đều mặc định, và hệ sinh thái plugin bên dưới là dành riêng cho Figma.
Figma plugin cho quy trình token
Figma Variables native đã bao phủ nhiều thứ mà trước đây cần plugin, nhưng Tokens Studio for Figma (trước đây là Figma Tokens) vẫn được dùng rộng rãi, đặc biệt khi team cần:
- Một nguồn token dựa trên JSON nằm trong git repository (thay vì chỉ nằm trong file
.fig), để việc thay đổi token được PR review, diff, và versioning giống như code. - Token set và theme — khả năng gộp nhiều lớp token (core → semantic → component) và đổi theme (light/dark, brand A/B) mà không cần trùng lặp mọi giá trị.
- Đồng bộ hai chiều: push token từ JSON vào Figma Variables, hoặc pull các chỉnh sửa thực hiện trong Figma ngược ra JSON, thường qua một repo GitHub/GitLab kết nối sẵn.
- Phép tính và tham chiếu ngay trong giá trị token (
{spacing.core.4} * 2), điều mà Figma Variables native hỗ trợ kém linh hoạt hơn.
Pattern thực tế mà hầu hết team áp dụng: giá trị token nằm trong một (hoặc vài) file JSON trong cùng repository với thư viện component, Tokens Studio đồng bộ JSON đó vào Figma để designer làm việc với variable sống, và Style Dictionary (bên dưới) tiêu thụ cùng JSON đó để sinh ra artifact code. Một nguồn, hai bên tiêu thụ.
Định dạng file: JSON như định dạng interchange
JSON là định dạng interchange trên thực tế (de facto) cho design token vì nó có thể diff được trong git, không bị vendor lock-in, và mọi tool downstream (Figma plugin, Style Dictionary, script tùy chỉnh) đều parse được mà không cần SDK độc quyền. W3C Design Tokens Community Group (DTCG) format đang nổi lên như một chuẩn hình dạng cho JSON này để các tool ngừng tự bịa ra các dialect không tương thích:
{
"color": {
"brand": {
"primary": { "$value": "#3B82F6", "$type": "color" },
"primary-hover": { "$value": "#2563EB", "$type": "color" }
}
},
"space": {
"4": { "$value": "16px", "$type": "dimension" }
}
}
Tổ chức file token thường chia theo tier (xem Design Token & Nền tảng thị giác cho mô hình tier core/semantic/component):
tokens/
core.json # raw scales: color.blue.500, space.4, font.size.16
semantic.json # aliases: color.brand.primary -> color.blue.500
components.json # component-scoped: button.padding-x -> space.4
Tổ chức file design: file library vs. file product
Một design system trong Figma hầu như luôn được chia thành ít nhất hai loại file:
- File library (đôi khi gọi là file “UI Kit” hoặc “Component Library”): thuộc sở hữu của team design-system, chỉ chứa các master component, variant của chúng, và các frame documentation. File này được publish thành một Figma library để các file khác tiêu thụ qua
Assets→ remote component. - File design product: thuộc sở hữu của team product/feature, chứa các màn hình thực tế được compose gần như hoàn toàn từ các instance của component lấy từ library đã publish, cộng với layout riêng của từng màn hình.
Sự phân chia này quan trọng về mặt vận hành: nếu mọi product designer tự vẽ lại button ở local thay vì kéo instance từ library, thì khoảnh khắc library được cập nhật (ví dụ corner radius đổi trên toàn hệ thống), những button vẽ tay đó sẽ không bao giờ cập nhật theo — chính xác là vấn đề drift mà toàn bộ bài này tồn tại để ngăn chặn. Figma thể hiện điều này qua thông báo tường minh “N components have updates” khi source của một library instance thay đổi, đây là một trong số ít nơi design tooling đưa ra tín hiệu chủ động, tự động rằng có gì đó đã trôi dạt — gần giống điều mà package manager làm cho dependency của code hơn bất cứ thứ gì design tooling từng có một thập kỷ trước.
UI kit trong Figma như bản phản chiếu của thư viện đã code
UI kit là artifact phía Figma nên được đánh giá bằng một câu hỏi duy nhất: compose một màn hình bằng nó có tạo ra thứ mà engineer có thể build bằng thư viện đã code mà không cần dịch lại không? Điều đó đòi hỏi UI kit phải phản chiếu chính xác các component đã code:
| API component đã code | Component tương ứng trong Figma |
|---|---|
<Button variant="primary" size="md" disabled /> | Component set Button với variant property variant, size, state |
<Alert status="warning"> | Component set Alert với variant property status |
| Slot/children prop cho icon | Một slot instance component lồng bên trong component Figma, có thể swap được |
| Responsive breakpoint trong CSS | Variant frame/component tương ứng theo từng breakpoint, hoặc auto-layout resize tương đương |
Khi UI kit chậm chân hơn thư viện đã code — một size="xs" mới ship trong code nhưng chưa được thêm thành variant Figma — designer hoặc không thể design state đó, hoặc tệ hơn, tự vẽ tay, mở lại vấn đề drift. Giữ cho hai bên đồng bộ là vấn đề governance và quy trình cũng nhiều như là vấn đề tooling (xem Governance & Đóng góp), nhưng yêu cầu về tooling là danh sách variant của UI kit nên được coi như được sinh ra/dẫn xuất từ cùng một contract component với code, chứ không được thiết kế độc lập.
Thư viện component đã code và Storybook
Thư viện đã code là phần triển khai thực sự — một package React, Vue, hoặc Web Components mà các codebase product cài đặt và import. Storybook đã trở thành công cụ chuẩn để phát triển và document các component này ở trạng thái cô lập, vì một lý do cụ thể: nó cho phép bạn render một component ở mọi trạng thái và mọi tổ hợp props mà không cần một ứng dụng chạy đầy đủ, một backend, hay dữ liệu thực. Sự cô lập đó là điều khiến nó hữu ích cả như một công cụ phát triển (build component trong một sandbox nhanh, tập trung) lẫn công cụ documentation (instance Storybook đã deploy chính là catalog component sống mà designer, PM, và engineer khác duyệt qua).
Một story Button tối giản, dùng Component Story Format 3 (CSF3) với @storybook/react:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'destructive'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
},
args: {
children: 'Button',
variant: 'primary',
size: 'md',
disabled: false,
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: 'primary' },
};
export const Secondary: Story = {
args: { variant: 'secondary' },
};
export const Destructive: Story = {
args: { variant: 'destructive' },
};
export const Disabled: Story = {
args: { disabled: true },
};
// One story rendering every variant/size combination at once,
// useful for visual regression snapshots and design review.
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{(['primary', 'secondary', 'destructive'] as const).map((variant) =>
(['sm', 'md', 'lg'] as const).map((size) => (
<Button key={`${variant}-${size}`} variant={variant} size={size}>
{variant} / {size}
</Button>
)),
)}
</div>
),
};
Vài điểm ví dụ này minh họa mà đặc biệt quan trọng với quy trình design-to-dev:
argTypesvớicontrol: 'select'sinh ra một control panel tương tác trong UI của Storybook — designer hoặc PM có thể đổivariantvàsizetrực tiếp mà không cần đọc code, đây là tương đương gần nhất phía code với việc đổi variant property trong Figma.- Mục
tags: ['autodocs']tự động sinh trang documentation từ TypeScript props và JSDoc comment của component, giảm bớt (dù không loại bỏ hoàn toàn) gánh nặng viết documentation riêng đã đề cập trong Guideline Component & Mẫu tài liệu. - Story
AllVariantslà thứ mà một tool visual-regression (Chromatic, Percy, hoặc test runner của chính Storybook kết hợp Playwright) chụp snapshot ở mỗi PR, bắt được drift visual ngoài ý muốn trước khi ship — tương đương phía code của thông báo “component has updates” của Figma.
Token pipeline tooling: Style Dictionary
Khi token đã tồn tại dưới dạng JSON, phải có gì đó biến nguồn duy nhất đó thành định dạng gốc của mọi nền tảng: CSS custom properties cho web, .xml/hằng số Kotlin cho Android, .swift/.plist cho iOS, và đôi khi xuất ngược lại thành JSON tiêu thụ được bởi Figma. Style Dictionary (ban đầu được xây dựng tại Amazon, giờ được dùng rộng rãi) là công cụ phổ biến nhất cho việc này: bạn định nghĩa token một lần, cấu hình một hoặc nhiều “platform,” và nó chạy các transform để sinh ra từng định dạng output.
Một config style-dictionary cơ bản và nguồn token:
// tokens/color.json
{
"color": {
"brand": {
"primary": { "value": "#3B82F6" },
"primary-hover": { "value": "#2563EB" }
},
"text": {
"default": { "value": "#111827" },
"muted": { "value": "#6B7280" }
}
},
"space": {
"1": { "value": "4px" },
"2": { "value": "8px" },
"4": { "value": "16px" },
"8": { "value": "32px" }
}
}
// style-dictionary.config.js
const StyleDictionary = require('style-dictionary');
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'build/css/',
files: [
{
destination: 'variables.css',
format: 'css/variables',
options: { outputReferences: true },
},
],
},
ios: {
transformGroup: 'ios-swift',
buildPath: 'build/ios/',
files: [
{
destination: 'StyleDictionary.swift',
format: 'ios-swift/class.swift',
className: 'StyleDictionary',
},
],
},
android: {
transformGroup: 'android',
buildPath: 'build/android/',
files: [
{
destination: 'colors.xml',
format: 'android/colors',
},
{
destination: 'dimens.xml',
format: 'android/resources',
},
],
},
json: {
// A flat JSON re-export, e.g. for a Figma plugin (Tokens Studio)
// to pull built values back into the design tool.
transformGroup: 'js',
buildPath: 'build/json/',
files: [
{
destination: 'tokens.json',
format: 'json/flat',
},
],
},
},
};
Chạy style-dictionary build --config style-dictionary.config.js sẽ sinh ra, trong số các file khác, một build/css/variables.css đại loại như:
:root {
--color-brand-primary: #3B82F6;
--color-brand-primary-hover: #2563EB;
--color-text-default: #111827;
--color-text-muted: #6B7280;
--space-1: 4px;
--space-2: 8px;
--space-4: 16px;
--space-8: 32px;
}
…và các khai báo native tương đương cho iOS và Android trong cùng một bước build, từ cùng một file nguồn. Đây chính là cơ chế cho phép designer đổi một giá trị JSON và, sau khi CI build sinh lại mọi output nền tảng, thay đổi đó chạm tới web, iOS, và Android cùng lúc thay vì ba engineer phải tự tay cập nhật ba file constants riêng biệt. Xem Design Token & Nền tảng thị giác để biết cách tổ chức các tier token mà pipeline này tiêu thụ.
Analytics cho component và tooling (ngắn gọn)
Một khi component và token đã tồn tại, một câu hỏi khác nảy sinh: chúng có thực sự được dùng không, và ai dùng? Instrument analytics về import/usage (component nào render nhiều nhất trong production, giá trị token nào thực sự được tham chiếu so với giá trị “chết” trong file design, component nào có bản tái triển khai không chính thức gần-trùng lặp) biến “chúng tôi nghĩ component này đang được dùng” thành một sự thật đo lường được, và là điều thiết yếu để ưu tiên bảo trì hoặc deprecation. Chủ đề này được bàn sâu hơn trong Đo lường thành công: Analytics & Testing; phiên bản ngắn gọn ở đây là: các lựa chọn tooling ở giai đoạn trước trong pipeline (một registry package code duy nhất, một instance Storybook duy nhất, tên token có cấu trúc) chính là thứ khiến việc đo lường đó khả thi về sau.
Best Practices
Chọn tech stack cho thư viện đã code
Quyết định tooling lớn nhất, khó đảo ngược nhất là thư viện component đã code được xây dựng bằng gì, và nó nên được dẫn dắt bởi số lượng frontend stack mà tổ chức thực sự đang chạy, chứ không phải bởi framework mà team design-system ưa thích.
| Cách tiếp cận | Mô tả | Phù hợp nhất | Đánh đổi |
|---|---|---|---|
| Framework-specific (ví dụ: chỉ React, hoặc package React + package Vue riêng biệt) | Component được build trực tiếp theo model component của một framework, dùng pattern native của nó (hooks, composition API, slot) | Tổ chức đã chuẩn hóa trên một frontend framework duy nhất | Build nhanh nhất, DX idiomatic theo framework tốt nhất, nhưng một stack thứ hai (giả sử một micro-frontend Vue) hoặc phải có package song song riêng, hoặc không được hệ thống hỗ trợ chút nào |
| Framework-agnostic (Web Components) | Component được build bằng API Custom Elements / Shadow DOM native, sau đó các package wrapper mỏng phơi ra API idiomatic theo từng framework (wrapper React, wrapper Vue) trên cùng một element nền tảng | Tổ chức lớn với nhiều frontend stack, hoặc tầm nhìn dài hạn khi framework lựa chọn có thể thay đổi | Web Components lịch sử có những góc cạnh thô ráp hơn về form participation, SSR, và typing TypeScript so với một component React native, và bản thân lớp wrapper là công sức phụ để build và bảo trì |
| Nhiều nguồn framework, một thiết kế duy nhất | Duy trì các bản triển khai React và Vue (v.v.) thực sự riêng biệt, nhưng sinh ra cả hai từ cùng một nguồn token/spec và giữ chúng theo cùng một contract visual qua các test visual-regression chung | Tổ chức cần ergonomics native theo từng framework ở mỗi stack và đủ khả năng chịu chi phí bảo trì trùng lặp | Chi phí cao nhất; chỉ hợp lý khi wrapper “framework-agnostic” tỏ ra không đủ idiomatic với một team cụ thể |
Một quy tắc kinh nghiệm: nếu hiện tại, và trong tương lai gần có thể dự đoán được, chỉ có đúng một frontend framework trên toàn bộ danh mục sản phẩm, thì build framework-specific gần như luôn là lựa chọn thực dụng — Web Components giải quyết một vấn đề bạn chưa gặp phải, với chi phí duy trì thực sự liên tục. Ngay khi một framework thứ hai xuất hiện (một vụ acquisition mang theo codebase Angular, một kiến trúc micro-frontend, một hybrid mobile-web), framework-agnostic bắt đầu đáng giá, nhưng ngay cả khi đó, nhiều team chọn pattern “wrapper” (Lit hoặc Web Components thuần bên dưới, các package wrapper React/Vue/Angular mỏng bên trên) thay vì bắt mọi team tiêu thụ phải viết markup Custom Element thô.
Giữ nguồn token là nơi duy nhất giá trị được chỉnh sửa thủ công
Mọi artifact downstream — Figma Variables, CSS custom properties, hằng số iOS/Android — nên được sinh ra, không bao giờ chỉnh sửa thủ công. Nếu một engineer nhận thấy một giá trị hex sai và “sửa luôn” trong variables.css mà không đụng vào JSON nguồn, thì lần build Style Dictionary tiếp theo sẽ âm thầm ghi đè (revert) bản sửa của họ và drift lại quay về từ hướng khác. Coi các thư mục output đã sinh ra như build artifact (thường được literally gitignore, sinh ra trong CI) chứ không phải file mà ai đó chỉnh sửa trực tiếp.
Version hóa token pipeline và thư viện component cùng nhau, nhưng không cần ràng buộc cadence release của chúng một cách không cần thiết
Một thay đổi token gây breaking (đổi tên color.brand.primary thành color.brand.default, ví dụ) nên tuân theo cùng kỷ luật semantic-versioning như một thay đổi breaking API component — xem Versioning & Quản lý Release. Vì token được tiêu thụ bởi cả UI kit lẫn thư viện đã code, một bump version major của token thường buộc phải có một release phối hợp trên cả hai phía, đó là lý do tốt để giữ package token nhỏ, ổn định, và thay đổi ít thường xuyên hơn từng component riêng lẻ.
Biến Storybook, chứ không phải file Figma, thành artifact được review cho việc triển khai
Review thiết kế diễn ra trong Figma; review triển khai nên diễn ra dựa trên một instance Storybook đã deploy (hoặc một bản preview deployment riêng cho từng PR), chứ không chỉ bằng cách đọc lại diff code. Điều này cho designer một cách để sign-off độ chính xác về visual mà không cần đọc TypeScript, và cho reviewer QA/accessibility một bề mặt ổn định, cô lập để test — xem Accessibility & Thiết kế bao trùm để biết cần kiểm tra gì trong review đó.
Tự động hóa tín hiệu “library has updates” ở cả hai phía
Ở phía design, dựa vào thông báo cập nhật library native của Figma thay vì audit thủ công xem ai đang dùng component đã lỗi thời. Ở phía code, coi mỗi release mới của thư viện component giống như bất kỳ bump dependency nào khác — PR tự động (kiểu Renovate/Dependabot), changelog, và diff visual-regression trước khi merge, chứ không phải một tin nhắn Slack thủ công kiểu “này, chúng tôi vừa ship v2, cập nhật khi nào rảnh nhé” mà chẳng ai hành động theo.
Tài liệu tham khảo
Part of the Design System Roadmap knowledge base.
Overview
Every hand-off between a designer and an engineer is an opportunity for drift. Without a shared vocabulary, a designer draws a button with 12px of padding and a #3B82F6 fill; the engineer builds it with 10px and #3B72F5 because that’s what the last ticket used. Multiply that by hundreds of screens and dozens of engineers, and a product slowly disintegrates into thousands of near-identical, slightly-wrong components. This isn’t a discipline problem — it’s a tooling problem. Nobody can hold “the exact right spacing scale” in their head across a hundred files.
A design system fixes this by making the system itself the source of truth, and tooling is what keeps design and code pointing at that same source instead of drifting into two parallel realities. This note covers the concrete toolchain that makes that possible: the design editor (almost always Figma today) and the plugins that turn it from a drawing tool into a token-aware system; the interchange formats (chiefly JSON) that let tokens flow between design and code; the UI kit that mirrors the coded library inside Figma; Storybook as the workbench where components are actually built and documented in code; Style Dictionary as the pipeline that turns one token source into every platform’s native format; and the tech-stack decision (framework-specific vs. framework-agnostic) that determines how far a single coded library can reach across an organization.
None of these tools are the design system — they’re the plumbing that makes a design system operationally real instead of a static PDF of guidelines nobody follows.
Fundamentals
The hand-off problem, precisely
“Design-to-dev hand-off” traditionally means: a designer finishes a Figma file, an engineer opens it, and the engineer manually reads off values — colors, spacing, font sizes, corner radii — and re-encodes them by hand into CSS or a StyleSheet object. Every one of those reads is a chance for a rounding error, a mis-click, or a “close enough” approximation. Worse, the two artifacts (the Figma file and the codebase) have no structural connection: if the designer nudges a spacing token next sprint, nothing tells the engineer their component is now stale.
A mature design system tooling chain removes as many of these manual re-encodings as possible:
| Manual hand-off step | Tooling replacement |
|---|---|
| Designer picks a color visually, engineer eyeballs the hex from Figma’s inspector | Both reference the same named token (color.brand.primary) exported from one source |
| Engineer guesses spacing from a ruler | Spacing tokens (space.4 = 16px) shared via Style Dictionary output |
| Designer redraws a button from scratch for a new screen | Designer drags a Button component instance from a shared Figma library with the same variants as the coded component |
| Engineer builds a component, hopes it matches design intent | Engineer builds against Storybook, designer reviews the deployed Storybook instance instead of a static mock |
| ”Is this component already built somewhere?” asked in Slack | Component/token usage analytics show what already exists (see Measuring Success: Analytics & Testing) |
Where each tool sits in the pipeline
It helps to think of the toolchain as a pipeline with a single source of truth flowing in two directions — into the design tool and into the codebase:
- Token sourceJSON / Tokens Studio / DTCG
- Figma (design)via Tokens Studio plugin, syncs to Figma VariablesStyle Dictionarybuild pipeline
- Figma UI Kitmirrors coded component APICSS custom propertiesiOS (Swift/.plist)Android (XML/Kotlin)
- designer composes screensProduct design files
- Coded component libraryReact / Vue / Web Components
- developed & documented inStorybook
The two halves — design and code — never talk to each other directly. They both consume the same token source and (ideally) the same component specification, which keeps them honest without requiring a live API between Figma and your repo.
Key Concepts
Figma as the dominant design editor
Figma has become the default design tool for teams building design systems, for reasons that matter specifically to systems work rather than general UI design:
- Components and variants. A Figma component can have multiple variants (size:
sm/md/lg, state:default/hover/disabled, intent:primary/secondary) collapsed into one component set with a property panel — this maps almost one-to-one onto a coded component’s prop API (<Button size="lg" variant="primary" />). A designer picking variants in Figma’s right-hand panel is doing the same mental operation as an engineer setting props. - Auto-layout. Figma’s auto-layout (flex-like sizing, padding, gap, resizing behavior) mirrors CSS flexbox/grid closely enough that a well-built auto-layout frame translates almost directly into a CSS layout, instead of an engineer reverse-engineering pixel positions from a static frame.
- Figma Variables. Native support for typed variables (color, number, string, boolean) with mode switching (e.g., light/dark, density) that map directly onto design tokens — including aliasing (a semantic token referencing a core token) and cross-file libraries.
- Dev Mode. A dedicated inspection mode for engineers that surfaces the underlying variable/token names, spacing, and (via plugins) generates code snippets, instead of forcing engineers into the same canvas designers use to draw.
None of this is unique to Figma forever, but as of today it’s the tool almost every design-system reference (including roadmap.sh) assumes, and the plugin ecosystem below is Figma-specific.
Figma plugins for token workflows
Figma’s native Variables cover a lot of what used to require a plugin, but Tokens Studio for Figma (formerly Figma Tokens) remains widely used, especially where teams need:
- A JSON-based token source that lives in a git repository (instead of only inside a
.figfile), so token changes get the same PR review, diff, and versioning as code. - Token sets and themes — the ability to compose multiple token layers (core → semantic → component) and swap themes (light/dark, brand A/B) without duplicating every value.
- Two-way sync: push tokens from JSON into Figma Variables, or pull edits made in Figma back out into JSON, typically via a connected GitHub/GitLab repo.
- Math and references inside token values (
{spacing.core.4} * 2), which native Figma Variables support less flexibly.
The practical pattern most teams land on: token values live in a JSON file (or a small set of them) in the same repository as the component library, Tokens Studio syncs that JSON into Figma so designers work with live variables, and Style Dictionary (below) consumes the same JSON to generate code artifacts. One source, two consumers.
File formats: JSON as the interchange format
JSON is the de facto interchange format for design tokens because it is diffable in git, has no vendor lock-in, and every downstream tool (Figma plugins, Style Dictionary, custom scripts) can parse it without a proprietary SDK. The W3C Design Tokens Community Group (DTCG) format is emerging as a standard shape for this JSON so tools stop inventing incompatible dialects:
{
"color": {
"brand": {
"primary": { "$value": "#3B82F6", "$type": "color" },
"primary-hover": { "$value": "#2563EB", "$type": "color" }
}
},
"space": {
"4": { "$value": "16px", "$type": "dimension" }
}
}
Token file organization typically splits by tier (see Design Tokens & Visual Foundations for the core/semantic/component tier model):
tokens/
core.json # raw scales: color.blue.500, space.4, font.size.16
semantic.json # aliases: color.brand.primary -> color.blue.500
components.json # component-scoped: button.padding-x -> space.4
Design file organization: library file vs. product files
A design system in Figma is almost always split into at least two kinds of files:
- The library file (sometimes called the “UI Kit” or “Component Library” file): owned by the design-system team, contains only the master components, their variants, and documentation frames. It is published as a Figma library so other files can consume it via
Assets→ remote components. - Product design files: owned by product/feature teams, contain actual screens composed almost entirely of instances of components pulled from the published library, plus screen-specific layout.
This split matters operationally: if every product designer redraws buttons locally instead of pulling instances from the library, the moment the library updates (say, the corner radius changes system-wide), those hand-drawn buttons never update — exactly the drift problem this whole note exists to prevent. Figma surfaces this as an explicit “N components have updates” notification when a library instance’s source changes, which is one of the few places design tooling gives a proactive, automatic signal that something has drifted — closer to what a package manager does for code dependencies than anything design tooling had a decade ago.
The Figma UI kit as a mirror of the coded library
The UI kit is the Figma-side artifact that should be judged by one question: does composing a screen with it produce something an engineer can build with the coded library without translation? That requires the UI kit to mirror the coded components exactly:
| Coded component API | Figma component equivalent |
|---|---|
<Button variant="primary" size="md" disabled /> | Component set Button with variant properties variant, size, state |
<Alert status="warning"> | Component set Alert with variant property status |
| Slot/children prop for icon | A nested component instance slot inside the Figma component, swappable |
| Responsive breakpoints in CSS | Matching frame/component variants sized per breakpoint, or auto-layout that resizes equivalently |
When the UI kit lags behind the coded library — a new size="xs" ships in code but isn’t added as a Figma variant — designers either can’t design that state or, worse, draw it by hand, reopening the drift problem. Keeping the two in lockstep is a governance and process question as much as a tooling one (see Governance & Contribution), but the tooling requirement is that the UI kit’s variant list should be treated as generated/derived from the same component contract as the code, not designed independently.
The coded component library and Storybook
The coded library is the actual implementation — a React, Vue, or Web Components package that product codebases install and import. Storybook has become the standard tool for developing and documenting these components in isolation, for a specific reason: it lets you render one component in every state and prop combination without needing a full running application, a backend, or realistic data. That isolation is what makes it useful both as a development tool (build the component against a fast, focused sandbox) and a documentation tool (the deployed Storybook instance is the living component catalog designers, PMs, and other engineers browse).
A minimal Button story, using Component Story Format 3 (CSF3) with @storybook/react:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'destructive'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
},
args: {
children: 'Button',
variant: 'primary',
size: 'md',
disabled: false,
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: 'primary' },
};
export const Secondary: Story = {
args: { variant: 'secondary' },
};
export const Destructive: Story = {
args: { variant: 'destructive' },
};
export const Disabled: Story = {
args: { disabled: true },
};
// One story rendering every variant/size combination at once,
// useful for visual regression snapshots and design review.
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{(['primary', 'secondary', 'destructive'] as const).map((variant) =>
(['sm', 'md', 'lg'] as const).map((size) => (
<Button key={`${variant}-${size}`} variant={variant} size={size}>
{variant} / {size}
</Button>
)),
)}
</div>
),
};
A few things this example demonstrates that matter for design-to-dev workflow specifically:
argTypeswithcontrol: 'select'generates an interactive control panel in Storybook’s UI — a designer or PM can flipvariantandsizelive without reading code, which is the closest code-side equivalent to flipping a variant property in Figma.- The
tags: ['autodocs']entry auto-generates a documentation page from the component’s TypeScript props and JSDoc comments, reducing (though not eliminating) the separate documentation-writing burden covered in Component Guidelines & Documentation Patterns. - The
AllVariantsstory is what a visual-regression tool (Chromatic, Percy, or Storybook’s own test runner with Playwright) snapshots on every PR, catching unintended visual drift before it ships — the code-side counterpart to Figma’s “component has updates” notification.
Token pipeline tooling: Style Dictionary
Once tokens exist as JSON, something has to turn that single source into every platform’s native format: CSS custom properties for web, .xml/Kotlin constants for Android, .swift/.plist for iOS, and sometimes a re-export back into Figma-consumable JSON. Style Dictionary (originally built at Amazon, now widely adopted) is the most common tool for this: you define your tokens once, configure one or more “platforms,” and it runs transforms to produce each output format.
A basic style-dictionary config and token source:
// tokens/color.json
{
"color": {
"brand": {
"primary": { "value": "#3B82F6" },
"primary-hover": { "value": "#2563EB" }
},
"text": {
"default": { "value": "#111827" },
"muted": { "value": "#6B7280" }
}
},
"space": {
"1": { "value": "4px" },
"2": { "value": "8px" },
"4": { "value": "16px" },
"8": { "value": "32px" }
}
}
// style-dictionary.config.js
const StyleDictionary = require('style-dictionary');
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'build/css/',
files: [
{
destination: 'variables.css',
format: 'css/variables',
options: { outputReferences: true },
},
],
},
ios: {
transformGroup: 'ios-swift',
buildPath: 'build/ios/',
files: [
{
destination: 'StyleDictionary.swift',
format: 'ios-swift/class.swift',
className: 'StyleDictionary',
},
],
},
android: {
transformGroup: 'android',
buildPath: 'build/android/',
files: [
{
destination: 'colors.xml',
format: 'android/colors',
},
{
destination: 'dimens.xml',
format: 'android/resources',
},
],
},
json: {
// A flat JSON re-export, e.g. for a Figma plugin (Tokens Studio)
// to pull built values back into the design tool.
transformGroup: 'js',
buildPath: 'build/json/',
files: [
{
destination: 'tokens.json',
format: 'json/flat',
},
],
},
},
};
Running style-dictionary build --config style-dictionary.config.js produces, among other files, a build/css/variables.css roughly like:
:root {
--color-brand-primary: #3B82F6;
--color-brand-primary-hover: #2563EB;
--color-text-default: #111827;
--color-text-muted: #6B7280;
--space-1: 4px;
--space-2: 8px;
--space-4: 16px;
--space-8: 32px;
}
…and equivalent native declarations for iOS and Android in the same build step, from the same source file. This is the mechanism that lets a designer change one JSON value and, after a CI build regenerates all platform outputs, have that change reach web, iOS, and Android simultaneously instead of three engineers manually updating three separate constants files. See Design Tokens & Visual Foundations for how to structure the token tiers this pipeline consumes.
Component and tooling analytics (brief)
Once components and tokens exist, a separate question emerges: are they actually used, and by whom? Instrumenting import/usage analytics (which components render most often in production, which token values are actually referenced versus dead in the design file, which components have near-duplicate unofficial reimplementations) turns “we think this component is used” into a measurable fact, and is essential for prioritizing maintenance or deprecation. This is covered in depth in Measuring Success: Analytics & Testing; the short version here is that tooling choices earlier in the pipeline (a single coded package registry, a single Storybook instance, structured token names) are exactly what make that instrumentation possible later.
Best Practices
Choosing a tech stack for the coded library
The single biggest, hardest-to-reverse tooling decision is what the coded component library is built in, and it should be driven by how many frontend stacks the organization actually runs, not by which framework the design-system team prefers.
| Approach | Description | Best fit | Tradeoff |
|---|---|---|---|
| Framework-specific (e.g., React-only, or a React package + a separate Vue package) | Components built directly against one framework’s component model, using its native patterns (hooks, composition API, slots) | Organizations standardized on a single frontend framework | Fastest to build, best framework-idiomatic DX, but a second stack (say, a Vue micro-frontend) either gets its own parallel package or no system support at all |
| Framework-agnostic (Web Components) | Components built with the native Custom Elements / Shadow DOM APIs, then thin wrapper packages expose framework-idiomatic APIs (React wrapper, Vue wrapper) over the same underlying element | Larger organizations with multiple frontend stacks, or long time horizons where the framework of choice may change | Web Components historically have rougher edges around form participation, SSR, and TypeScript typing than a native React component, and the wrapper layer is itself extra work to build and maintain |
| Multi-framework source, single design | Maintain genuinely separate React and Vue (etc.) implementations, but generate both from the same token/spec source and hold them to the same visual contract via shared visual-regression tests | Organizations that need framework-native ergonomics in each stack and can afford the duplicated maintenance cost | Highest cost; only justified when “framework-agnostic” wrappers prove insufficiently idiomatic for a given team |
A rule of thumb: if there is currently, and will foreseeably be, exactly one frontend framework across the product portfolio, building framework-specific is almost always the pragmatic choice — Web Components solve a problem you don’t have yet, at a real ongoing cost. The moment a second framework enters the picture (an acquisition bringing an Angular codebase, a micro-frontend architecture, a mobile-web hybrid), framework-agnostic starts paying for itself, but even then, many teams choose the “wrapper” pattern (Lit or vanilla Web Components under the hood, thin React/Vue/Angular wrapper packages on top) rather than asking every consuming team to write raw Custom Element markup.
Keep the token source as the only place values are hand-edited
Every downstream artifact — Figma Variables, CSS custom properties, the iOS/Android constants — should be generated, never hand-edited. If an engineer notices a wrong hex value and “just fixes it” in variables.css without touching the JSON source, the next Style Dictionary build silently reverts their fix and the drift returns from a different direction. Treat generated output directories as build artifacts (often literally gitignored, generated in CI) rather than files anyone edits directly.
Version the token pipeline and the component library together, but don’t couple their release cadence unnecessarily
A breaking token change (renaming color.brand.primary to color.brand.default, for instance) should follow the same semantic-versioning discipline as a breaking component API change — see Versioning & Release Management. Because tokens are consumed by both the UI kit and the coded library, a major token version bump often forces a coordinated release across both, which is a good reason to keep the token package small, stable, and change it less often than individual components change.
Make Storybook, not the Figma file, the reviewable artifact for implementation
Design review happens in Figma; implementation review should happen against a deployed Storybook instance (or a PR-specific preview deployment of it), not by re-reading code diffs alone. This gives designers a way to sign off on visual accuracy without needing to read TypeScript, and gives QA/accessibility reviewers a stable, isolated surface to test against — see Accessibility & Inclusive Design for what to check for in that review.
Automate the “library has updates” signal on both sides
On the design side, rely on Figma’s native library-update notifications rather than manual audits of who’s using stale components. On the code side, treat a new component-library release the same way you’d treat any dependency bump — automated PRs (Renovate/Dependabot-style), changelogs, and visual-regression diffs before merging, not a manual “hey, we shipped v2, please update when you get a chance” Slack message that never gets actioned.