Documentation: Cách viết documentation để Dev hiểu đúng ý đồ thiết kế
7/19/2026 · 14p đọc
title: "Documentation: Cách viết documentation để Dev hiểu đúng ý đồ thiết kế"
series: "UX Architect: 40 bài Expert-Deep Dive"
part: "Phần 3 — UI Design System & Tính nhất quán"
order: 30
audience: "Product Designer & UX Architect"
reading_time: "14 phút"
tags: ["design documentation", "design handoff", "Storybook", "living documentation", "design-to-code", "component annotation"]
Documentation: Cách viết documentation để Dev hiểu đúng ý đồ thiết kế
Một designer bàn giao file Figma cho đội kỹ thuật: 12 màn hình, đầy đủ trạng thái default, hover, và một dòng ghi chú ngắn "theo design này nhé". Hai tuần sau, QA report bug: khi người dùng nhấn nút "Lưu" trong lúc đang loading, hệ thống cho phép nhấn thêm một lần nữa, tạo ra hai request trùng lặp và hai bản ghi trùng trong database. Designer xem lại thì sững người — trong đầu họ, nút này đương nhiên phải bị disable khi đang loading, đó là điều "hiển nhiên" tới mức không ai nghĩ cần phải viết ra.
Đây không phải là lỗi của dev cẩu thả. Đây là hệ quả tất yếu của việc bàn giao thiết kế bằng ảnh tĩnh (static artifact) thay vì bằng đặc tả hành vi (behavioral specification). File Figma trả lời câu hỏi "trông như thế nào" (what) cực kỳ tốt — bố cục, màu sắc, khoảng cách đều đo được chính xác tới từng pixel. Nhưng nó gần như câm lặng trước câu hỏi "phải hoạt động như thế nào" (how it behaves) và "tại sao lại thiết kế như vậy" (why) — hai câu hỏi mà kỹ sư bắt buộc phải tự trả lời bằng cách đoán, khi họ ngồi viết code cho từng trạng thái, từng nhánh rẽ, từng edge case mà bản thiết kế không hề vẽ ra.
Bài này khép lại Phần 3 của series — phần bàn về Design System và tính nhất quán — bằng câu hỏi thực dụng nhất: làm sao để tài liệu thiết kế trở thành một hợp đồng rõ ràng (contract) giữa designer và kỹ sư, thay vì một tấm ảnh đẹp mà mỗi người đọc theo một cách?
The UX Problem
Vấn đề gốc rễ: thiết kế được bàn giao dưới dạng hình ảnh tĩnh của trạng thái mặc định (default state), trong khi phần lớn độ phức tạp thực sự của một sản phẩm nằm ở các trạng thái không-mặc-định — loading, error, empty, disabled, partial data, race condition khi người dùng thao tác nhanh hơn hệ thống phản hồi.
Biểu hiện cụ thể trong thực tế:
- File Figma có đúng một frame cho form đăng ký: trạng thái trống, chưa nhập gì. Không có frame nào cho: đang submit (loading), submit thất bại vì mạng lỗi, submit thất bại vì email đã tồn tại, submit thành công nhưng redirect chậm.
- Designer vẽ một nút "Xóa" màu đỏ, nhưng không ghi rõ: bấm xong có cần confirm dialog không? Nếu record đang được người khác chỉnh sửa đồng thời thì sao? Nút có disable trong lúc gọi API xóa không?
- Một dropdown chọn nhiều (multi-select) được vẽ với 3 item đã chọn — nhưng không ai biết giới hạn tối đa là bao nhiêu, và khi vượt giới hạn thì UI phản hồi thế nào (chặn chọn thêm, hay hiện toast lỗi, hay tự động bỏ item cũ nhất).
- Dev hỏi designer trên Slack "cái này khi loading thì làm gì?", designer trả lời bằng một câu ngắn không kèm ảnh, hai tuần sau không ai còn nhớ cuộc hội thoại đó, và designer tiếp theo join team không hề biết quyết định đó tồn tại.
Vì sao lỗi này phổ biến đến mức gần như là mặc định của ngành? Vì công cụ thiết kế (Figma và tương đương) được tối ưu để vẽ ra một khung hình tĩnh tại một thời điểm, không phải để mô tả một cỗ máy trạng thái (state machine) vận hành theo thời gian. Designer suy nghĩ bằng hình ảnh và trực giác tương tác — với họ, "nút bị disable khi loading" là một phản xạ ngầm định đã có sẵn trong đầu từ hàng trăm sản phẩm họ từng dùng qua. Nhưng kỹ sư không đọc được trực giác đó từ một tấm ảnh tĩnh; họ chỉ đọc được đúng những gì được vẽ ra hoặc viết ra thành chữ. Khoảng trống giữa "điều designer ngầm hiểu" và "điều thực sự được ghi lại thành văn bản" chính là nơi mọi bug logic tương tác sinh ra — và tệ nhất, nó chỉ lộ diện ở giai đoạn QA, khi chi phí sửa đã cao gấp nhiều lần so với lúc còn là một dòng chú thích trong tài liệu thiết kế.
Design Framework
Framework giải quyết vấn đề này là một cấu trúc bàn giao đầy đủ (Complete Handoff Documentation), gồm bốn lớp bắt buộc — thiếu bất kỳ lớp nào, tài liệu vẫn để lại khoảng trống cho dev tự đoán:
Lớp 1 — Visual Spec (đã có sẵn trong Figma)
Kích thước, khoảng cách, màu sắc, typography của trạng thái mặc định. Đây là phần hầu hết designer đã làm tốt — không phải trọng tâm của bài này.
Lớp 2 — State Annotation (chú thích trạng thái)
Với mỗi thành phần tương tác, liệt kê toàn bộ trạng thái có thể xảy ra, không chỉ trạng thái đẹp nhất. Với một component bất kỳ, checklist trạng thái tối thiểu là:
| Trạng thái | Câu hỏi cần trả lời |
|---|---|
| Default | Trông như thế nào khi chưa có tương tác |
| Hover / Focus | Phản hồi thị giác khi con trỏ/bàn phím tới gần |
| Loading | Trong lúc chờ phản hồi hệ thống, UI hiện gì, có chặn thao tác lặp không |
| Disabled | Khi nào bị vô hiệu hóa, và vì sao (quyền hạn, điều kiện nghiệp vụ, đang xử lý) |
| Error | Lỗi hiển thị ở đâu, dạng gì (inline, toast, modal), có thể retry không |
| Empty | Khi chưa có dữ liệu, hiển thị gì thay vì một khoảng trắng |
| Success/Confirmation | Sau khi hành động thành công, người dùng nhận phản hồi gì |
| Partial/Edge case | Dữ liệu không đầy đủ, tên quá dài, số 0, giá trị âm, mất kết nối giữa chừng |
Lớp 3 — Interaction Logic (logic tương tác dưới dạng luồng)
Không phải hình ảnh, mà là luật chuyển trạng thái — dùng lưu đồ đơn giản hoặc bảng if-then. Ví dụ: "Nếu đang ở trạng thái loading VÀ người dùng nhấn nút lần nữa → bỏ qua sự kiện, không gửi request thứ hai." Đây là phần quan trọng nhất và bị bỏ sót nhiều nhất, vì nó đòi hỏi designer suy nghĩ như một kỹ sư viết state machine, không chỉ như một họa sĩ vẽ khung hình.
Lớp 4 — Rationale (lý do đằng sau quyết định — "why")
Với mỗi quyết định không hiển nhiên, ghi lại tại sao chọn cách này chứ không phải cách khác. Ví dụ: "Chọn confirm dialog thay vì undo-toast cho hành động xóa, vì đây là xóa vĩnh viễn không thể khôi phục (hard delete), khác với các hành động xóa mềm (soft delete) khác trong sản phẩm." Lớp này cực kỳ quan trọng vì nó ngăn dev "tối ưu hóa" sai — một kỹ sư thấy pattern xóa không nhất quán giữa các module, tự ý đổi sang pattern quen thuộc mà không biết rằng sự khác biệt đó là chủ đích, không phải sơ suất.
Bốn lớp này áp dụng được với bất kỳ công cụ nào — Figma comment, một file Markdown đi kèm, hay một trang wiki. Công cụ không quan trọng bằng việc bốn lớp thông tin có tồn tại và có được đặt cạnh visual spec hay không.
The "Architect's" Insight
Đây là nơi tài liệu thiết kế ngừng là chuyện quy trình làm việc (workflow) và trở thành một quyết định kiến trúc về nguồn sự thật (source of truth).
Vấn đề cấu trúc của tài liệu Figma tĩnh không nằm ở chỗ nó thiếu chi tiết — mà ở chỗ nó là một bản sao (copy), tách rời hoàn toàn khỏi nơi hành vi thực sự được định nghĩa: mã nguồn. Một file Figma, dù được chú thích kỹ tới đâu ở thời điểm bàn giao, vẫn chỉ là ảnh chụp trạng thái ý định thiết kế tại một mốc thời gian. Nó không có cơ chế nào tự động phản ánh khi component thực tế trong codebase thay đổi. Khi một dev sáu tháng sau sửa logic disable của nút Button để thêm một điều kiện nghiệp vụ mới, không ai bắt buộc quay lại cập nhật file Figma — và 99% trường hợp, không ai làm việc đó, vì nó nằm ngoài luồng công việc (workflow) bình thường của một pull request.
Kết quả kiến trúc: tồn tại hai nguồn sự thật (dual source of truth) — Figma nói một chuyện, code chạy trên production nói một chuyện khác — và theo thời gian, khoảng cách giữa hai nguồn này chỉ có tăng, không bao giờ tự giảm, vì không có cơ chế đồng bộ tự động.
Giải pháp kiến trúc đúng là đảo ngược hướng phụ thuộc: thay vì tài liệu mô tả code, hãy để tài liệu được sinh ra trực tiếp từ code — một dạng "tài liệu sống" (living documentation) mà công cụ đọc chính component thật (props, type definition, các biến thể được định nghĩa trong code) và tự động render ra một trang xem trực quan mọi trạng thái. Về mặt kỹ thuật, cách làm phổ biến là viết các "story" (kịch bản hiển thị) cạnh chính file component — mỗi story gọi component thật với một tổ hợp props khác nhau (variant="danger", state="loading", disabled={true}), và công cụ tài liệu tự dựng thành một thư viện xem được, tương tác được, ngay từ mã nguồn đang chạy.
Hệ quả kiến trúc lan ra ba lớp:
Tính bất khả xâm phạm của đồng bộ (sync-by-construction): vì tài liệu được sinh từ chính type definition và component logic thật (ví dụ một union type
type ButtonVariant = 'primary' | 'secondary' | 'danger'trong TypeScript), nếu một dev thêm biến thể mới vào type nhưng không thêm story minh họa, hệ thống biên dịch hoặc lint có thể được cấu hình để cảnh báo thiếu tài liệu — biến "tài liệu đầy đủ" từ một kỳ vọng quy trình (dễ bị bỏ qua khi deadline gấp) thành một ràng buộc có thể kiểm tra được (checkable constraint), giống hệt cách semantic color token được ràng buộc bằng TypeScript exhaustiveness đã bàn ở bài trước.Loại bỏ nguồn sai lệch giữa design và implementation: khi designer muốn xác minh "component Button hiện tại trông như thế nào ở trạng thái loading", họ không còn cần hỏi dev hay đoán qua ảnh Figma cũ — họ mở trực tiếp trang tài liệu sống, xem đúng component đang chạy thật trên production, với đúng props thật. Điều này triệt tiêu hoàn toàn lớp rủi ro "Figma nói một đằng, code chạy một nẻo" vì không còn hai bản sao độc lập — chỉ còn một bản duy nhất, được nhìn từ hai góc.
Chi phí bảo trì tài liệu chuyển từ thủ công sang một phần của quy trình phát triển bình thường: viết một story mới cho một biến thể component không phải là một tác vụ "làm tài liệu" tách biệt cần lịch trình riêng — nó là một phần tự nhiên của việc code component đó, thường nằm trong cùng pull request, được review cùng lúc với code logic. Điều này giải quyết trực diện pitfall lớn nhất của tài liệu tĩnh: viết một lần rồi bỏ quên, vì bản thân quy trình phát triển đã ép tài liệu phải cập nhật cùng nhịp với code, không cần một người nhớ ra và làm thêm một việc ngoài luồng.
Nói cách khác: khi một UX Architect quyết định tài liệu bàn giao nên "sống" ở đâu, họ thực chất đang quyết định xem hệ thống có tồn tại một hay hai nguồn sự thật cho hành vi giao diện. Một nguồn duy nhất, gắn liền với code đang chạy, là quyết định kiến trúc đúng — nó biến tài liệu từ một sản phẩm phụ dễ lỗi thời thành một phần không thể tách rời của chính hệ thống.
Practical Example
Giả sử component Button trong design system của một CRM có các biến thể sau, định nghĩa trong code (ví dụ tối giản theo TypeScript/React):
// components/Button/Button.types.ts
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant: ButtonVariant;
size: ButtonSize;
disabled?: boolean;
loading?: boolean; // khi true: hiện spinner, tự động disable, chặn onClick lặp
onClick: () => void;
}
Tài liệu sống cho component này được viết cạnh chính file component, dưới dạng các kịch bản (story) khai báo tường minh mọi tổ hợp trạng thái cần minh họa:
// components/Button/Button.stories.tsx
export const AllVariants = () => (
<Grid>
{(['primary', 'secondary', 'danger', 'ghost'] as ButtonVariant[]).map((variant) => (
<Button key={variant} variant={variant} size="md" onClick={() => {}}>
{variant}
</Button>
))}
</Grid>
);
export const LoadingState = () => (
<Button variant="primary" size="md" loading onClick={() => {}}>
Đang lưu...
</Button>
// Chú thích đi kèm: "Khi loading=true, onClick KHÔNG được gọi dù người dùng
// click nhiều lần — xem logic chặn double-submit trong Button.tsx dòng 24."
);
export const DisabledVsLoading = () => (
<>
<Button variant="danger" size="md" disabled onClick={() => {}}>Xóa (disabled)</Button>
<Button variant="danger" size="md" loading onClick={() => {}}>Xóa (loading)</Button>
{/* Chú thích: disabled dùng khi điều kiện nghiệp vụ chưa đủ (chưa chọn item);
loading dùng khi request đang chạy. Hai trạng thái nhìn gần giống nhau
nhưng ý nghĩa khác nhau — không dùng lẫn. */}
</>
);
So sánh hai luồng bàn giao:
| Figma tĩnh | Tài liệu sống từ code | |
|---|---|---|
| Nguồn dữ liệu | Ảnh vẽ tay, cập nhật thủ công | Chính component đang chạy trên production |
| Khi thêm biến thể mới | Designer phải nhớ vẽ thêm frame | Dev thêm story trong cùng PR chứa code |
| Rủi ro lỗi thời | Cao — không ai bắt buộc đồng bộ lại | Thấp — tài liệu tự phản ánh code hiện tại |
| Dev xác minh hành vi loading | Hỏi lại designer hoặc đoán | Mở trang tài liệu, tương tác trực tiếp |
| Chú thích "why" | Nằm rải rác trong Slack/comment Figma | Nằm ngay cạnh story, review cùng code |
Điểm mấu chốt: cả hai cách đều có thể "đẹp" như nhau về mặt trình bày, nhưng chỉ cách thứ hai đảm bảo tính đúng đắn (correctness) theo thời gian, vì nó không thể lỗi thời mà không ai nhận ra — component đổi, story render theo component, sai lệch lộ ra ngay lập tức trên chính trang tài liệu.
Common Pitfalls
Chỉ bàn giao ảnh tĩnh của trạng thái mặc định, bỏ trống mọi trạng thái động. Designer nghĩ "trạng thái loading/error thì hiển nhiên rồi, dev tự biết phải làm sao" — nhưng "hiển nhiên" trong đầu designer không tự động chuyển thành code đúng. Hệ quả là mỗi dev tự đoán một kiểu, dẫn tới sự thiếu nhất quán y hệt vấn đề token màu rời rạc đã bàn ở Bài 24: cùng một khái niệm ("đang xử lý"), mỗi màn hình một cách hiện thực khác nhau vì không ai có đặc tả chung để tuân theo.
Viết tài liệu một lần lúc bàn giao ban đầu, rồi không bao giờ quay lại cập nhật khi thiết kế thay đổi. Đây là lỗi nguy hiểm hơn cả việc không có tài liệu — vì dev vẫn tin tưởng làm theo một tài liệu đã sai, thay vì biết rằng mình đang thiếu thông tin và cần hỏi lại. Một tài liệu cũ, sai, nhưng trông có vẻ đáng tin, còn tệ hơn không có tài liệu nào, vì nó tạo cảm giác an toàn giả (false confidence) cho cả hai phía.
Nhầm lẫn giữa "annotation nhiều chữ" và "annotation đúng chỗ". Một số designer phản ứng thái quá với pain point này bằng cách viết chú thích dài dòng khắp mọi ngóc ngách của file Figma — nhưng nếu chú thích không được cấu trúc theo state/logic/rationale rõ ràng (như framework ở trên), dev vẫn phải đọc lướt tìm thông tin cần, dễ bỏ sót đúng phần quan trọng nhất (ví dụ logic chặn double-submit) giữa hàng chục ghi chú về khoảng cách pixel không ảnh hưởng tới đúng-sai chức năng.
Checklist
- Mọi component/màn hình tương tác đều có đủ 8 trạng thái tối thiểu được minh họa: default, hover/focus, loading, disabled, error, empty, success, edge case — không chỉ trạng thái mặc định.
- Logic chuyển trạng thái (interaction logic) được viết thành luật tường minh dạng if-then hoặc lưu đồ, không để dev tự suy luận từ hình ảnh tĩnh.
- Mọi quyết định không hiển nhiên đều kèm lý do (rationale) — trả lời "why", không chỉ "what".
- Ưu tiên đặt story/tài liệu minh họa cạnh chính component thật trong codebase, thay vì chỉ giữ trong file thiết kế tách biệt.
- Thêm biến thể/props mới cho component đi kèm cập nhật story trong cùng pull request — không tách thành tác vụ "làm tài liệu sau".
- Đã kiểm tra: khi component thực tế thay đổi hành vi, tài liệu có tự động phản ánh hay cần một người nhớ cập nhật thủ công.
- Trước khi coi bàn giao là hoàn tất, tự hỏi: nếu dev không được hỏi thêm câu nào, họ có đủ thông tin implement đúng mọi trạng thái, hay vẫn phải đoán ít nhất một chỗ?
🔗 Bài viết liên quan
- Design System — vị trí của tài liệu sống trong toàn bộ hệ sinh thái design system.
- Error Handling & Edge Cases — cách tư duy đầy đủ các trạng thái lỗi/edge case cần đặc tả trong tài liệu bàn giao.
Bài trước: Prototyping · Bài tiếp theo: UX Writing