Dogfood: Korean fintech mobile app — design system bootstrap¶
Scenario: A 가계부 (personal finance tracker) mobile app for Korean millennials/Gen-Z. Brand-new product, no existing design system. Single-founder team — needs to ship MVP in 6 weeks.
Inputs given to the agent: - Product type: fintech, consumer mobile, Korean primary - Brand: undecided — wants "trustworthy + approachable + youthful" - Stack: React Native + Tailwind via NativeWind, no MUI/Ant - Light + dark mode
Skill used:
design-system-builder(which itself orchestratescolor-palette,component-spec-writerpatterns)Output: this document — a complete bootstrap deliverable a frontend dev could implement against without follow-up.
1. Foundations¶
Why this palette¶
Korean fintech consumer apps live or die on trust and approachability. Trust says cool blue. Approachability says soft saturation, never neon. Youthful says not corporate-Samsung-blue (too cold) — closer to Toss's coral-brand or Kakao Bank's golden warmth, but distinct.
Picked: teal-600 primary (#0D9488). Reasoning:
- Cool enough to read as financial / trustworthy.
- Warm enough to not feel sterile (closer to nature than pure blue).
- Differentiated from the dominant Korean fintech blues (Toss #3182F6, KakaoBank #FFEB00, NH Bank green #10B981).
- Pairs cleanly with a coral accent for "money in" affordances and slate neutrals for surfaces.
Tradeoff accepted: teal is less established as "money color" than blue or green. We claim it intentionally — differentiation matters more than instant-recognition for an MVP.
Tokens — color (light)¶
Generated via color-palette skill, citing knowledge/colors/color-theory.md, knowledge/colors/palettes-by-product-type.md, knowledge/a11y/contrast.md.
:root {
/* Primary — teal */
--color-primary-50: #F0FDFA;
--color-primary-100: #CCFBF1;
--color-primary-200: #99F6E4;
--color-primary-300: #5EEAD4;
--color-primary-400: #2DD4BF;
--color-primary-500: #14B8A6;
--color-primary-600: #0D9488; /* brand anchor — 5.6:1 on white ✓ */
--color-primary-700: #0F766E;
--color-primary-800: #115E59;
--color-primary-900: #134E4A;
--color-primary-950: #042F2E;
/* Accent — coral (money-in / positive amount) */
--color-accent-50: #FFF1F2;
--color-accent-500: #F43F5E;
--color-accent-600: #E11D48; /* 4.8:1 on white ✓ */
/* Neutrals — warm-leaning slate (cleaner with Hangul) */
--color-neutral-50: #F8FAFC;
--color-neutral-100: #F1F5F9;
--color-neutral-200: #E2E8F0;
--color-neutral-300: #CBD5E1;
--color-neutral-400: #94A3B8;
--color-neutral-500: #64748B;
--color-neutral-600: #475569;
--color-neutral-700: #334155;
--color-neutral-800: #1E293B;
--color-neutral-900: #0F172A;
/* Semantic — light */
--color-bg-default: #FFFFFF;
--color-bg-elevated: #F8FAFC; /* cards */
--color-bg-subtle: #F1F5F9; /* section bg, hover */
--color-text-primary: #0F172A; /* 18.7:1 on white */
--color-text-secondary: #475569; /* 7.4:1 */
--color-text-tertiary: #64748B; /* 4.9:1 — passes AA body */
--color-text-disabled: #94A3B8; /* aria-disabled only */
--color-border-default: #E2E8F0;
--color-border-strong: #CBD5E1;
--color-divider: #F1F5F9;
--color-primary-default: #0D9488;
--color-primary-hover: #0F766E;
--color-primary-active: #115E59;
--color-primary-subtle-bg: #F0FDFA;
--color-on-primary: #FFFFFF;
--color-focus-ring: #5EEAD4; /* primary-300 — 3:1 on both white and primary */
/* Money-specific (fintech essential) */
--color-money-positive: #16A34A; /* income, deposits — green-600 */
--color-money-negative: #DC2626; /* expense, withdrawal — red-600 */
--color-money-neutral: #475569; /* unchanged amount, transfer */
--color-success: #16A34A;
--color-success-subtle-bg: #F0FDF4;
--color-warning: #D97706;
--color-warning-subtle-bg: #FFFBEB;
--color-error: #DC2626;
--color-error-subtle-bg: #FEF2F2;
--color-info: #0D9488; /* matches primary */
}
Tokens — color (dark)¶
Recomputed, not inverted. Cite knowledge/colors/color-theory.md for the recompute approach.
.dark {
--color-bg-default: #020617; /* near-black with slate hue */
--color-bg-elevated: #0F172A;
--color-bg-subtle: #1E293B;
--color-text-primary: #F8FAFC; /* 17.4:1 on bg-default */
--color-text-secondary: #CBD5E1;
--color-text-tertiary: #94A3B8;
--color-text-disabled: #64748B;
--color-border-default: #334155;
--color-border-strong: #475569;
--color-divider: #1E293B;
--color-primary-default: #2DD4BF; /* primary-400 — pops on dark */
--color-primary-hover: #5EEAD4;
--color-primary-active: #99F6E4;
--color-primary-subtle-bg: #042F2E; /* primary-950 */
--color-on-primary: #042F2E;
--color-focus-ring: #2DD4BF;
--color-money-positive: #4ADE80; /* green-400 — chroma boost for low-light eye */
--color-money-negative: #F87171; /* red-400 */
--color-money-neutral: #94A3B8;
--color-success: #4ADE80;
--color-warning: #FBBF24;
--color-error: #F87171;
--color-info: #2DD4BF;
}
Contrast matrix¶
| Pair (light) | Ratio | AA body | AA UI |
|---|---|---|---|
| text-primary on bg-default | 18.7:1 | ✓ | — |
| text-secondary on bg-default | 7.4:1 | ✓ | — |
| text-tertiary on bg-default | 4.9:1 | ✓ | — |
| primary-default on bg-default | 5.6:1 | ✓ | — |
| on-primary on primary-default | 8.0:1 | ✓ | — |
| focus-ring on bg-default | 3.1:1 | — | ✓ |
| focus-ring on primary-default | 3.0:1 | — | ✓ |
| money-positive on bg-default | 4.5:1 | ✓ | — |
| money-negative on bg-default | 4.8:1 | ✓ | — |
| Pair (dark) | Ratio | AA body | AA UI |
|---|---|---|---|
| text-primary on bg-default | 17.4:1 | ✓ | — |
| text-secondary on bg-default | 11.6:1 | ✓ | — |
| primary-default on bg-default | 7.2:1 | ✓ | — |
| focus-ring on bg-default | 7.2:1 | — | ✓ |
Typography¶
Korean primary → Pretendard. Cite knowledge/i18n/korean-typography.md for stack rationale.
:root {
--font-family-base: 'Pretendard', -apple-system, BlinkMacSystemFont,
'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
--font-family-mono: 'Pretendard JetBrains Mono', ui-monospace, Menlo, monospace;
/* Numerals — tabular for amounts */
--font-feature-amount: 'tnum' 1;
/* Scale — base 15 for mobile-primary, ratio 1.25 (major third) */
--font-size-xs: 12px;
--font-size-sm: 13px;
--font-size-base: 15px;
--font-size-lg: 17px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 30px;
--font-size-4xl: 38px; /* hero amount displays */
/* Line heights — Korean +10% from Latin defaults */
--line-height-tight: 1.25;
--line-height-base: 1.6; /* body — Korean adjustment */
--line-height-relaxed: 1.75;
/* Weights */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600; /* body emphasis — Korean reads thin at 500 */
--font-weight-bold: 700;
}
Variants:
| Token | Family | Weight | Size | LH | LS | Use |
|---|---|---|---|---|---|---|
display |
base | 700 | 38px | 1.2 | -0.01em | hero amounts, balance display |
heading-lg |
base | 700 | 24px | 1.3 | 0 | screen titles |
heading-md |
base | 600 | 20px | 1.4 | 0 | section titles |
heading-sm |
base | 600 | 17px | 1.4 | 0 | card titles |
body-lg |
base | 400 | 17px | 1.6 | 0 | important body |
body |
base | 400 | 15px | 1.6 | 0 | default body |
body-sm |
base | 400 | 13px | 1.6 | 0 | secondary text |
caption |
base | 400 | 12px | 1.5 | 0 | timestamps, metadata |
amount |
base | 600 | varies | 1.2 | -0.01em | monetary amounts (tnum) |
amount variant has font-feature-settings: 'tnum' so digits align in columns (essential for transaction lists). Cite knowledge/typography/type-scale-fundamentals.md.
Spacing¶
4-base scale per knowledge/layout/spacing-and-grid.md:
:root {
--space-2xs: 2px;
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-base: 16px;
--space-lg: 20px;
--space-xl: 24px;
--space-2xl: 32px;
--space-3xl: 48px;
--space-4xl: 64px;
}
Density bias for Korean consumer apps: higher than Western defaults (cite knowledge/i18n/korean-product-conventions.md). List items use --space-md vertical padding (not --space-base); cards use --space-base (not --space-lg).
Radius¶
:root {
--radius-sm: 6px;
--radius-md: 10px; /* default — friendly but not toy */
--radius-lg: 16px; /* cards, sheets */
--radius-xl: 24px; /* hero modules */
--radius-full: 9999px; /* pills, avatars */
}
Elevation¶
Mobile-first: avoid heavy shadows (battery, GPU). Use border-default for separation; reserve shadow for floating UI.
:root {
--shadow-none: none;
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 0 0 1px rgba(15, 23, 42, 0.04); /* very soft border-shadow combo */
--shadow-popover: 0 12px 24px rgba(15, 23, 42, 0.10); /* dropdown, popover */
--shadow-modal: 0 24px 48px rgba(15, 23, 42, 0.18); /* sheet, modal */
}
.dark {
--shadow-card: none; /* in dark, use --color-bg-elevated for separation, no shadow */
--shadow-popover: 0 12px 24px rgba(0, 0, 0, 0.5);
--shadow-modal: 0 24px 48px rgba(0, 0, 0, 0.6);
}
Motion¶
Cite knowledge/motion/principles.md:
:root {
--motion-fast: 150ms;
--motion-default: 250ms;
--motion-slow: 400ms;
--easing-out: cubic-bezier(0, 0, 0.2, 1); /* entrances */
--easing-in: cubic-bezier(0.4, 0, 1, 1); /* exits */
--easing-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* position changes */
--easing-emphasized: cubic-bezier(0.2, 0, 0, 1); /* hero moments */
}
@media (prefers-reduced-motion: reduce) {
:root {
--motion-fast: 0ms;
--motion-default: 0ms;
--motion-slow: 0ms;
}
}
2. Component baseline¶
Picked: shadcn-ui (NativeWind/Tailwind compatible variants).
Reasoning: - Stack mandate (NativeWind) → tokens-via-CSS-variables, not theme-prop pattern. - Solo founder → wants to own the components, not depend on a library that locks the API. - Korean market → expect to override copy, validation rules, payment integrations heavily; copy-paste suits. - Speed → shadcn add-by-name is the fastest path to "shipped".
Rejected: - MUI: ThemeProvider-based, doesn't translate cleanly to React Native. - Ant Design Mobile: powerful but Chinese-market-coded; less common in Korean consumer apps; aesthetic doesn't fit modern Korean fintech. - NativeBase / Tamagui: Tailwind-adjacent but smaller community in Korea.
For React Native specifically: shadcn-ui has community RN ports (gluestack-ui is the closest 1:1, or use shadcn web → adapt). Spec the components per skills/component-spec-writer/PLAYBOOK.md and pick the implementation per platform.
3. Starter component set (v0.1.0)¶
The minimum to ship MVP. Each one is either derived (use shadcn baseline + token override) or custom (needs its own spec — fintech-specific).
Derived from shadcn baseline¶
- Button — see examples/component-button.md
- Input — see examples/component-input.md
- Modal / Sheet — see examples/component-modal.md (use Sheet variant for mobile)
- Toast — see examples/component-toast.md
- Card — see examples/component-card.md
- Badge, Tag
- Tabs (bottom tab bar specifically)
- Switch, Checkbox, Radio
- Skeleton
Custom — needs in-house spec (Phase 2 work)¶
- AmountInput — number-formatted, comma-separator, ₩ suffix, KRW-rounded validation. Most-used input in the app.
- TransactionListItem — composed of icon + category + memo + amount; tabular numerals; pressable.
- CategoryPicker — emoji + Korean label, scrollable horizontal pill list.
- DateRangeSelector — supports Korean expressions ("이번 달", "지난 달", "최근 7일").
- AccountCard — bank logo + nickname + masked account number + balance.
- AmountDisplay — large display variant for "balance" hero with breakdown chips.
- BiometricGate — fingerprint/face-id wall (Korean fintech expectation).
4. Foundations document — color usage¶
When to use each color:
--color-primary-default: primary CTA only ("이체하기", "확인", "다음"). One per screen.--color-money-positive: amount text when positive (income line, deposit, balance increase). NOT for buttons.--color-money-negative: amount text when negative (expense, withdrawal). NOT for "destructive" buttons — use--color-errorfor those.--color-error: validation errors, destructive button intent.--color-text-primary: body, neutral amounts.--color-accent: rarely — promotional banners, "new" badges. Not for amount semantics.
Don't:
- Don't use --color-primary-default for amount text. Money color is its own axis.
- Don't use --color-money-positive for success buttons — use --color-success. Money colors are noun-scoped (the amount is income), not adjective-scoped (the action succeeded).
- Don't use --color-error for the negative-amount text. Negative != bad.
5. Foundations document — Korean considerations¶
Cite knowledge/i18n/korean-product-conventions.md, knowledge/i18n/korean-publishing.md.
- Phone-first auth: KakaoTalk login as primary social, SMS verification for native sign-up. Apple/Google login below.
- 본인인증: integrate PASS or NICE for transactions over ₩500K (typical fintech threshold).
- Address: Daum Postcode API for any address field.
- Payment integrations: Toss Payments SDK (cards), KakaoPay/NaverPay (wallets), 휴대폰결제 (small amounts).
- Currency display:
1,200,000원(Korean consumer convention) or₩1,200,000(Western/fintech). Pick one — default to원suffix for consumer warmth. - Date format:
2026.05.07(compact) or2026년 5월 7일(formal). Default compact in tables, formal in transaction details. - Tone: polite/honorific in error messages ("오류가 발생했습니다"), casual in CTAs ("저장", not "저장하시겠습니까").
- Submission compliance: separate marketing-consent checkbox at signup; in-app permission disclosure popups before requesting Android permissions; GRAC age rating not needed (non-game).
6. Output structure (what to commit)¶
fintech-app/
├── tokens/
│ ├── source.json # Style Dictionary source (W3C DTCG)
│ └── README.md
├── src/
│ ├── styles/
│ │ ├── tokens.css # generated
│ │ └── tokens.tailwind.js # generated
│ ├── components/
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── AmountInput/ # custom
│ │ ├── TransactionListItem/ # custom
│ │ └── ...
│ └── theme/
│ └── ThemeProvider.tsx
├── docs/
│ ├── FOUNDATIONS.md # color, type, spacing, motion, iconography rules
│ ├── KOREAN.md # Korean-specific patterns
│ └── COMPONENTS.md # component inventory + status
└── style-dictionary.config.json
tokens/source.json is the canonical source. Style Dictionary builds the rest.
7. Hand-off — what eng needs to do¶
npm install -D style-dictionaryand copystyle-dictionary.config.json(see docs/TOKEN-SYNC.md).- Create
tokens/source.jsonfrom this document (use Style Dictionary V4 W3C format). - Run
npx style-dictionary build. Outputs land insrc/styles/. npx shadcn@latest add button input dialog sheet sonner card badge tabs switch checkbox radio skeleton— pulls baseline components.- For each baseline, override the CSS-vars usage to point at our token names (a 5-min find-replace per component).
- Implement custom components from the Phase 2 list. Each gets its own spec via
component-spec-writerskill. - Wire up Toss Payments / KakaoPay SDKs.
- Add Daum Postcode for address fields.
- Implement Korean publishing checklist before submission to App Store / Play.
Cited sources¶
- knowledge/colors/color-theory.md
- knowledge/colors/palettes-by-product-type.md
- knowledge/a11y/contrast.md
- knowledge/typography/type-scale-fundamentals.md
- knowledge/typography/font-pairings.md
- knowledge/layout/spacing-and-grid.md
- knowledge/motion/principles.md
- knowledge/i18n/korean-typography.md
- knowledge/i18n/korean-product-conventions.md
- knowledge/i18n/korean-publishing.md
- knowledge/components/INDEX.md
- skills/design-system-builder/PLAYBOOK.md
- skills/color-palette/PLAYBOOK.md
- docs/TOKEN-SYNC.md