03. Design System & Atomic Components — Vibe Coding Series

Xây design system trước khi code UI — dùng AI tạo design token, atomic component, và đảm bảo consistency xuyên suốt dự án.

Tôi đã bảo AI viết UI cho TaskFlow năm lần. Lần đầu, button màu indigo. Lần hai, màu blue. Lần ba, màu violet. Lần bốn, AI tự thêm gradient. Lần năm, tôi mở app lên và thấy một mớ hỗn độn — mỗi màn hình trông như được thiết kế bởi một người khác nhau.

Không phải AI làm sai. Tôi chưa bao giờ nói với nó style là gì.


Bối cảnh

Ở bài 02, tôi đã chốt API contract cho TaskFlow — OpenAPI spec, mock server, và AI_CONTEXT.md để AI agent có context rõ ràng. Bài này bước vào Phase 2.5: trước khi viết một dòng UI code nào, chúng ta cần một design system — ngôn ngữ thiết kế chung mà cả bạn lẫn AI đều hiểu và tuân theo.


Design token là nền tảng

Design token là những giá trị nguyên tử nhất của design system: màu sắc, typography, spacing, border radius, shadow. Thay vì nói “button màu xanh”, bạn nói “button dùng color.primary.600”. Thay vì “padding vừa phải”, bạn nói “padding spacing.4”.

Token giải quyết vấn đề gốc rễ: AI không có khái niệm “xanh của brand bạn” hay “spacing hợp lý” — nhưng nó hoàn toàn có thể tuân theo một hệ thống token cụ thể.

AI generate token từ brand brief

Tôi dùng prompt này để tạo token ban đầu cho TaskFlow:

Prompt:
"TaskFlow là task management SaaS cho indie team. Brand personality:
professional nhưng không corporate, clean, focus vào productivity.
Primary color: blue-ish. Tạo design token system với:
- Color palette (primary, neutral, semantic: success/warning/error)
- Typography scale (font size, weight, line height)
- Spacing scale (4px base)
- Border radius, shadow
Format: CSS custom properties."

AI trả về một token file đầy đủ. Tôi review, điều chỉnh một vài giá trị, và đây là kết quả cuối cùng cho TaskFlow:

/* tokens.css */
:root {
  /* Color — Primary */
  --color-primary-50: #eff6ff;
  --color-primary-100: #dbeafe;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;

  /* Color — Neutral */
  --color-neutral-0: #ffffff;
  --color-neutral-50: #f8fafc;
  --color-neutral-100: #f1f5f9;
  --color-neutral-200: #e2e8f0;
  --color-neutral-500: #64748b;
  --color-neutral-700: #334155;
  --color-neutral-900: #0f172a;

  /* Color — Semantic */
  --color-success: #22c55e;
  --color-warning: #f59e0b;
  --color-error: #ef4444;

  /* Typography */
  --font-size-xs: 0.75rem;    /* 12px */
  --font-size-sm: 0.875rem;   /* 14px */
  --font-size-base: 1rem;     /* 16px */
  --font-size-lg: 1.125rem;   /* 18px */
  --font-size-xl: 1.25rem;    /* 20px */
  --font-size-2xl: 1.5rem;    /* 24px */

  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;

  --line-height-tight: 1.25;
  --line-height-normal: 1.5;

  /* Spacing (4px base) */
  --spacing-1: 0.25rem;   /* 4px */
  --spacing-2: 0.5rem;    /* 8px */
  --spacing-3: 0.75rem;   /* 12px */
  --spacing-4: 1rem;      /* 16px */
  --spacing-6: 1.5rem;    /* 24px */
  --spacing-8: 2rem;      /* 32px */

  /* Border radius */
  --radius-sm: 0.25rem;
  --radius-md: 0.375rem;
  --radius-lg: 0.5rem;
  --radius-full: 9999px;

  /* Shadow */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}

Nếu bạn dùng Tailwind, AI có thể generate tailwind.config.ts với theme.extend tương đương — cùng logic, khác format.


Atomic component approach

Atomic Design chia component thành 3 tầng:

  • Atom: đơn vị nhỏ nhất, không thể chia nhỏ hơn — Button, Input, Badge, Typography
  • Molecule: kết hợp 2–3 atom — SearchBar (Input + Button), FormField (Label + Input + ErrorText)
  • Organism: kết hợp nhiều molecule — TaskCard, NavigationBar, TaskForm

Với AI, tôi chỉ generate atom trước. Molecule và organism sẽ được compose từ atom — AI sẽ tự biết cách kết hợp khi đã có atom đúng chuẩn.

Dùng AI generate component với token

Prompt pattern tôi dùng cho mỗi atom:

Prompt:
"Tạo React component [ComponentName] với:
- Dùng CSS custom properties từ tokens.css đã định nghĩa
- Props: [list props và type]
- Variants: [list variants]
- Accessibility: aria attributes phù hợp
- TypeScript, không dùng inline style"

Điểm quan trọng: tôi luôn mention “dùng CSS custom properties từ tokens.css” — đây là cách feed design system vào AI context ở cấp độ prompt.

Storybook để document

Sau khi có component, tôi yêu cầu AI generate Storybook story tương ứng:

Prompt:
"Tạo Storybook story cho Button component. Bao gồm stories cho
tất cả variants (primary, secondary, ghost), sizes (sm, md, lg),
và states (default, disabled, loading)."

Storybook không bắt buộc cho project nhỏ — nhưng nó là “living documentation” giúp bạn và AI đều thấy component trông như thế nào trước khi integrate vào app.


AI và consistency

Feed design system vào AI_CONTEXT.md

Sau khi có token và component library, tôi cập nhật AI_CONTEXT.md với section mới:

## Design System

### Tokens
- File: /src/styles/tokens.css
- Color: --color-primary-{50|100|500|600|700}, --color-neutral-{0|50|100|200|500|700|900}
- Semantic: --color-success, --color-warning, --color-error
- Spacing: --spacing-{1|2|3|4|6|8} (4px base)
- Typography: --font-size-{xs|sm|base|lg|xl|2xl}, --font-weight-{normal|medium|semibold}

### Component Library
- Atoms: Button, Input, Card, Badge, Typography
- Location: /src/components/ui/
- Usage: import từ @/components/ui, không tự tạo style mới

### Rules
- KHÔNG dùng hardcoded color value (ví dụ: #3b82f6) — luôn dùng token
- KHÔNG tạo component mới nếu atom đã có — compose từ existing component
- Mọi interactive element phải có focus state và aria label

Từ đây, mỗi khi AI generate UI code, nó có context đầy đủ về design system. Không còn chuyện AI tự bịa màu hay spacing.

Review AI-generated component

Khi AI trả về component, tôi check 3 điểm:

  1. Token compliance: có hardcoded value nào không? (#3b82f6 thay vì var(--color-primary-500))
  2. Accessibility: có aria-label, role, focus state không?
  3. Prop API: có nhất quán với các component khác không? (naming convention, variant pattern)

Dùng AI để review chính code AI vừa tạo:

Prompt:
"Review component này và kiểm tra:
1. Có dùng hardcoded color/spacing thay vì CSS token không?
2. Accessibility: thiếu aria attribute nào?
3. Prop API có nhất quán với Button component đã có không?"

Ví dụ thực chiến: 5 atom component cho TaskFlow

Đây là 5 atom tôi tạo cho TaskFlow, theo thứ tự:

1. Typography — đầu tiên vì mọi component khác đều dùng text

2. Badge — đơn giản nhất, chỉ có color variant

3. Button — phức tạp hơn, có variant, size, và loading state

4. Input — có error state, label, helper text

5. Card — container component, dùng shadow và border token

Đây là Button component — atom phức tạp nhất trong 5 cái:

// src/components/ui/Button.tsx
import type { ButtonHTMLAttributes } from "react";

type ButtonVariant = "primary" | "secondary" | "ghost";
type ButtonSize = "sm" | "md" | "lg";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
}

const variantStyles: Record<ButtonVariant, string> = {
  primary: "btn-primary",
  secondary: "btn-secondary",
  ghost: "btn-ghost",
};

const sizeStyles: Record<ButtonSize, string> = {
  sm: "btn-sm",
  md: "btn-md",
  lg: "btn-lg",
};

export function Button({
  variant = "primary",
  size = "md",
  loading = false,
  disabled,
  children,
  className = "",
  ...props
}: ButtonProps) {
  return (
    <button
      className={`btn ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
      disabled={disabled || loading}
      aria-busy={loading}
      {...props}
    >
      {loading ? <span className="btn-spinner" aria-hidden="true" /> : null}
      {children}
    </button>
  );
}
/* src/styles/components/button.css */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-2);
  font-weight: var(--font-weight-medium);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color 150ms, opacity 150ms;
}

.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn:focus-visible { outline: 2px solid var(--color-primary-500); outline-offset: 2px; }

.btn-primary {
  background: var(--color-primary-600);
  color: var(--color-neutral-0);
  border: none;
}
.btn-primary:hover:not(:disabled) { background: var(--color-primary-700); }

.btn-secondary {
  background: var(--color-neutral-0);
  color: var(--color-neutral-700);
  border: 1px solid var(--color-neutral-200);
}
.btn-secondary:hover:not(:disabled) { background: var(--color-neutral-50); }

.btn-ghost {
  background: transparent;
  color: var(--color-neutral-700);
  border: none;
}
.btn-ghost:hover:not(:disabled) { background: var(--color-neutral-100); }

.btn-sm { padding: var(--spacing-1) var(--spacing-3); font-size: var(--font-size-sm); }
.btn-md { padding: var(--spacing-2) var(--spacing-4); font-size: var(--font-size-base); }
.btn-lg { padding: var(--spacing-3) var(--spacing-6); font-size: var(--font-size-lg); }

Tất cả giá trị đều dùng token — không có hardcoded color hay spacing nào. Khi tôi muốn thay đổi primary color của toàn app, tôi chỉ cần sửa --color-primary-600 trong tokens.css.


Những điều cần tránh

Bỏ qua design system, để AI tự quyết style. Đây là cách nhanh nhất để có một app trông như patchwork. AI không có “gu thẩm mỹ” nhất quán — mỗi lần generate là một lần random. Dành 2–3 giờ setup token và 5 atom component sẽ tiết kiệm hàng chục giờ refactor sau này.

Over-engineer design system cho project nhỏ. Ngược lại, đừng build Figma token pipeline, design-to-code automation, và multi-theme support cho một MVP. Với project nhỏ, một file tokens.css và 5–7 atom component là đủ. Complexity có thể thêm sau khi bạn biết mình thực sự cần gì.


Tóm tắt

  • Design token là ngôn ngữ chung giữa bạn và AI — định nghĩa một lần, dùng ở khắp nơi
  • AI có thể generate token từ brand brief, nhưng bạn phải review và điều chỉnh
  • Atomic approach: build atom trước, compose molecule và organism sau
  • Cập nhật AI_CONTEXT.md với design system rules — đây là cách feed context hiệu quả nhất
  • Review AI-generated component: token compliance, accessibility, prop API consistency

Tiếp theo

Phase 3 giải quyết câu hỏi: khi đã có design system và API contract, làm thế nào để chia nhỏ feature thành task đúng granularity cho AI agent? Task quá lớn thì AI mất context, task quá nhỏ thì overhead cao. Bài tiếp theo đi vào task planning và test strategy.

→ Bài 04: Task Planning & Test Strategy

← Bài 02: Specs & API Contract First