# UI Design System — Toss TDS

> 어떤 스택에서든 동일한 톤을 이식하기 위한 단일 소스. 토스 TDS(mobile) 공개 문서의 토큰 체계를 웹 이식용으로 재해석한 문서다.
>
> **한 줄 정의**: 흰 배경 위 **10단계 그레이 스케일**과 **단일 블루(`#3182f6`)** 포인트, 넉넉한 12px+ 라운드로 구성된 **금융 슈퍼앱** 시스템. 색은 시각적 스케일(grey100, blue500)로 정의하되, 컴포넌트는 시맨틱 레이어(background, layeredBackground 등)를 통해서만 소비한다.

---

## 목차

1. [디자인 원칙](#1-디자인-원칙)
2. [색상 토큰](#2-색상-토큰)
3. [타이포그래피](#3-타이포그래피)
4. [레이아웃 & 라운드](#4-레이아웃--라운드)
5. [핵심 컴포넌트](#5-핵심-컴포넌트)
6. [인터랙션](#6-인터랙션)
7. [다크 모드](#7-다크-모드)
8. [접근성](#8-접근성)
9. [안티패턴](#9-안티패턴)
10. [프레임워크 어댑터](#10-프레임워크-어댑터)
11. [이식 가이드](#11-이식-가이드)

---

## 1. 디자인 원칙

| 원칙 | 정의 |
|---|---|
| **최소 품질 보장** | 시스템의 목적은 개성 표현이 아니라 **어떤 화면이든 일정 수준 이상의 품질**을 보장하는 것. |
| **스케일 → 시맨틱 2계층** | 원시 스케일(`grey100`, `blue500`)은 토큰 정의에만 쓰고, 컴포넌트는 시맨틱 토큰(`background`, `layeredBackground`)만 참조. |
| **단일 블루 포인트** | 액션·강조는 `blue500` 하나로 수렴. 나머지 유채색은 의미 전달(오류·성공·경고)에만. |
| **그레이가 위계를 만든다** | 텍스트 위계는 grey900 → grey600 → grey500 순. 색이 아닌 **그레이 농도**로 정보 우선순위를 표현. |
| **넉넉한 라운드** | 카드·버튼·입력 모두 12px 이상. 직각·1px 미만 라운드는 사용하지 않는다. |
| **보더보다 면** | 구분은 보더 대신 배경색 차이(`greyBackground`)와 여백으로. 보더는 입력 필드 등 최소한만. |
| **동적 타입 대응** | 타이포 토큰은 접근성 확대(더 큰 텍스트)를 전제로 추상화 — px 하드코딩 금지. |

---

## 2. 색상 토큰

### 2-1. 원시 스케일 (Palette)

```css
:root {
  /* Grey — 텍스트·배경·보더의 원천 */
  --grey-50:  #f9fafb;
  --grey-100: #f2f4f6;
  --grey-200: #e5e8eb;
  --grey-300: #d1d6db;
  --grey-400: #b0b8c1;
  --grey-500: #8b95a1;
  --grey-600: #6b7684;
  --grey-700: #4e5968;
  --grey-800: #333d4b;
  --grey-900: #191f28;

  /* Blue — 단일 액션 컬러 */
  --blue-50:  #e8f3ff;
  --blue-100: #c9e2ff;
  --blue-200: #90c2ff;
  --blue-300: #64a8ff;
  --blue-400: #4593fc;
  --blue-500: #3182f6;   /* 기준 포인트 */
  --blue-600: #2272eb;
  --blue-700: #1b64da;

  /* 의미 색상 (대표값) */
  --red-500:    #f04452;  /* 오류·경고, 스케일 #ffeeee~#a51926 */
  --orange-600: #e45600;  /* 주의 상한 */
  --green-600:  #027648;  /* 성공 상한 */
}
```

### 2-2. 시맨틱 토큰 — 컴포넌트가 실제로 쓰는 층

```css
:root {
  --background:          #ffffff;          /* 페이지 기본 배경 */
  --grey-background:     var(--grey-100);  /* 구분·비강조 영역 */
  --layered-background:  #ffffff;          /* 바텀시트·카드 표면 */
  --floated-background:  #ffffff;          /* 플로팅 요소 표면 */

  --text-primary:        var(--grey-900);  /* 제목·본문 */
  --text-secondary:      var(--grey-600);  /* 보조 설명 */
  --text-tertiary:       var(--grey-500);  /* 캡션·플레이스홀더 */
  --text-disabled:       var(--grey-400);

  --border:              var(--grey-200);
  --primary:             var(--blue-500);
  --primary-pressed:     var(--blue-600);
  --primary-surface:     var(--blue-50);   /* 옅은 블루 면 (선택 상태 등) */
}
```

### 2-3. 사용 규칙

| 역할 | 토큰 |
|---|---|
| 제목·금액·본문 | `var(--text-primary)` |
| 보조 설명 | `var(--text-secondary)` |
| 캡션·힌트 | `var(--text-tertiary)` |
| 페이지 배경 | `var(--background)` |
| 섹션 구분 배경 | `var(--grey-background)` |
| 카드·시트 표면 | `var(--layered-background)` + 그림자 |
| CTA·링크·선택 강조 | `var(--primary)` |
| 오류 | `var(--red-500)` |

> **핵심 규칙**: 컴포넌트 코드에 `--grey-300` 같은 원시 스케일을 직접 쓰지 않는다. 시맨틱 층을 거쳐야 다크 모드·리브랜딩이 한 곳에서 끝난다.

---

## 3. 타이포그래피

### 3-1. 폰트

```css
:root {
  --font-sans: 'Pretendard Variable', Pretendard,
               -apple-system, BlinkMacSystemFont,
               'Noto Sans KR', system-ui, sans-serif;
}

body {
  font-family: var(--font-sans);
  color: var(--text-primary);
  background: var(--background);
  letter-spacing: -0.02em;   /* 국문 조판: 살짝 좁게 */
}
```

```html
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
```

### 3-2. 타입 스케일 — Typography 1~7

숫자 계층 토큰. 행간은 크기의 약 1.35~1.5배로 고정되어 있다.

| 토큰 | `font-size` | `line-height` | 용도 |
|---|---|---|---|
| `t1` | 30px | 40px | 초대형 제목·핵심 금액 |
| `t2` | 26px | 35px | 큰 제목 |
| `t3` | 22px | 31px | 표준 제목 |
| `t4` | 20px | 29px | 작은 제목 |
| `t5` | 17px | 25.5px | 본문 |
| `t6` | 15px | 22.5px | 작은 본문·리스트 |
| `t7` | 13px | 19.5px | 캡션·라벨 |

Weight: Light / Regular / Medium / Semibold / Bold 5단계. 제목은 Bold, 본문은 Regular~Medium이 기본.

```css
.t1 { font-size: 1.875rem; line-height: 2.5rem;    font-weight: 700; }
.t3 { font-size: 1.375rem; line-height: 1.9375rem; font-weight: 700; }
.t5 { font-size: 1.0625rem; line-height: 1.59375rem; }
.t7 { font-size: 0.8125rem; line-height: 1.21875rem; color: var(--text-tertiary); }
```

### 3-3. 동적 타입 (접근성 확대)

- 크기는 `rem` 기반으로 작성해 사용자 글자 크기 설정(100%~310%)에 비례 확대되게 한다.
- 레이아웃은 텍스트 2배 확대에도 깨지지 않아야 한다 — 고정 높이 컨테이너에 텍스트를 넣지 않는다.

---

## 4. 레이아웃 & 라운드

```css
:root {
  --radius-card:   1.25rem;  /* 20px — 카드·바텀시트 */
  --radius-button: 0.75rem;  /* 12px — 버튼·입력 */
  --radius-chip:   62.4375rem; /* 풀 라운드 — 칩·뱃지 */
  --spacing-unit:  0.25rem;  /* 4px 배수 스페이싱 */
}

.container { max-width: 40rem; margin-inline: auto; padding-inline: 1.25rem; }
```

- 모바일 우선 단일 컬럼. 콘텐츠 폭은 좁게(640px) 유지.
- 섹션 구분은 `--grey-background` 면 + 12px 이상 여백. 수평선(`<hr>`) 사용 지양.

---

## 5. 핵심 컴포넌트

전체 카탈로그: Button, TextField, TextArea, Checkbox, Switch, Slider, Stepper, Badge, Toast, Tooltip, Skeleton, Progress, BottomSheet, BottomCTA, ListRow, Dialog, Modal, Keypad, Agreement 등 40여 종. 아래는 시그니처 4종.

### 5-1. BottomCTA — 화면 하단 고정 주 버튼

```css
.bottom-cta {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  padding: 0.75rem 1.25rem calc(0.75rem + env(safe-area-inset-bottom));
  background: var(--background);
}
.bottom-cta__button {
  width: 100%;
  padding: 1rem;
  border: none;
  border-radius: var(--radius-button);
  background: var(--primary);
  color: #ffffff;
  font-size: 1.0625rem;
  font-weight: 600;
  cursor: pointer;
}
.bottom-cta__button:active { background: var(--primary-pressed); }
.bottom-cta__button:disabled { background: var(--grey-200); color: var(--text-disabled); }
```

### 5-2. ListRow — 리스트 행 (정보 밀도의 기본 단위)

```html
<button class="list-row">
  <span class="list-row__icon">🏦</span>
  <span class="list-row__texts">
    <span class="list-row__title">토스뱅크 통장</span>
    <span class="list-row__sub">1,234,567원</span>
  </span>
  <span class="list-row__arrow">›</span>
</button>
```

```css
.list-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 1rem 1.25rem;
  background: transparent;
  border: none;
  text-align: left;
  cursor: pointer;
}
.list-row:active { background: var(--grey-100); }
.list-row__title { font-size: 1.0625rem; color: var(--text-primary); }
.list-row__sub   { font-size: 0.9375rem; color: var(--text-secondary); }
```

### 5-3. 카드 표면

```css
.card {
  background: var(--layered-background);
  border-radius: var(--radius-card);
  padding: 1.25rem;
  box-shadow: 0 1px 4px rgba(25, 31, 40, 0.06);
}
```

### 5-4. Toast

```css
.toast {
  position: fixed;
  bottom: 5rem;
  left: 50%;
  transform: translateX(-50%);
  background: var(--grey-800);
  color: #ffffff;
  padding: 0.75rem 1.25rem;
  border-radius: var(--radius-button);
  font-size: 0.9375rem;
}
```

---

## 6. 인터랙션

- 탭 피드백은 배경색 전환(`:active` → grey100)과 **살짝 눌리는 스케일**(`transform: scale(0.98)`, 0.1s)로.
- 전환은 `0.2s ease` 이하로 짧게. 스프링 계열 이징 선호.
- 스켈레톤 로딩을 기본 로딩 패턴으로 사용 — 스피너는 전체 화면 대기에만.

```css
.pressable { transition: transform 0.1s ease, background-color 0.2s ease; }
.pressable:active { transform: scale(0.98); }
```

---

## 7. 다크 모드

`.dark` 클래스 오버라이드. 그레이 스케일을 반전하되 **blue500 포인트는 유지**한다.

```css
.dark {
  color-scheme: dark;

  --background:         #101013;
  --grey-background:    #17171c;
  --layered-background: #1e1e24;
  --floated-background: #26262e;

  --text-primary:   #e5e8eb;
  --text-secondary: #8b95a1;
  --text-tertiary:  #6b7684;
  --text-disabled:  #4e5968;

  --border:          #333d4b;
  --primary:         #3182f6;
  --primary-pressed: #4593fc;   /* 다크에서는 눌림을 밝게 */
  --primary-surface: #1b2a45;
}
```

- 표면 계층(background → layered → floated)이 다크에서는 **밝아지는 방향**으로 쌓인다.
- 그림자 대신 표면 밝기 차이로 엘리베이션을 표현.

---

## 8. 접근성

- `--text-primary`(#191f28) 대비 15.6:1, `--text-secondary`(#6b7684) 4.9:1 — AA 통과.
- `--text-tertiary`(#8b95a1)는 캡션·플레이스홀더 전용. 본문 사용 금지 (대비 3.1:1).
- 모든 크기는 `rem` — 시스템 글자 확대에 반응해야 한다 (§3-3).
- 터치 타깃 최소 44×44px. ListRow·버튼은 패딩으로 보장.
- 색만으로 상태를 전달하지 않는다 — 오류는 색 + 아이콘 + 메시지 3중.

---

## 9. 안티패턴

| 금지 | 이유 |
|---|---|
| 원시 스케일 직접 참조 (`var(--grey-300)`) | 시맨틱 층 우회 — 다크 모드가 깨진다 |
| 블루 외 유채색을 액션에 사용 | 단일 포인트 원칙 |
| px 하드코딩 타이포 | 동적 타입 확대 대응 불가 |
| 직각 모서리·1px 라운드 | 12px+ 라운드가 브랜드 톤 |
| `<hr>`·보더로 섹션 구분 | 면(grey-background)과 여백으로 구분 |
| 고정 높이 텍스트 컨테이너 | 글자 확대 시 잘림 |
| 스피너 남용 | 스켈레톤이 기본 로딩 패턴 |
| 그레이 텍스트 4단계 초과 | 위계는 3단계(primary/secondary/tertiary)로 충분 |

---

## 10. 프레임워크 어댑터

### 10-1. Tailwind CSS v4

```css
@import "tailwindcss";

@theme {
  --color-background:     #ffffff;
  --color-grey-bg:        #f2f4f6;
  --color-layered:        #ffffff;
  --color-text-primary:   #191f28;
  --color-text-secondary: #6b7684;
  --color-text-tertiary:  #8b95a1;
  --color-border:         #e5e8eb;
  --color-primary:        #3182f6;
  --radius-card:          1.25rem;
  --radius-button:        0.75rem;
}

@custom-variant dark (&:where(.dark, .dark *));
```

### 10-2. Tailwind CSS v3

```js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        background: 'var(--background)',
        'grey-bg':  'var(--grey-background)',
        layered:    'var(--layered-background)',
        primary:    'var(--primary)',
        't-primary':   'var(--text-primary)',
        't-secondary': 'var(--text-secondary)',
        't-tertiary':  'var(--text-tertiary)',
      },
      borderRadius: { card: '1.25rem', button: '0.75rem' },
    },
  },
};
```

### 10-3. React / Vue / Svelte

CSS 변수가 원천. 다크 전환은 `document.documentElement.classList.toggle('dark')` 한 줄. 컴포넌트 스타일에서는 시맨틱 토큰만 참조한다.

---

## 11. 이식 가이드

### Step 1 — 토큰 2계층 복사

§2 원시 스케일 + 시맨틱 층, §7 다크 블록을 전역 CSS에 붙인다.

### Step 2 — Pretendard 연결

CDN 링크를 `<head>`에 추가하고 `letter-spacing: -0.02em`을 body에 설정.

### Step 3 — 시그니처 3종 적용

| 패턴 | 핵심 CSS |
|---|---|
| **BottomCTA** | 하단 고정 풀폭 블루 버튼, `border-radius: 12px` |
| **ListRow** | 아이콘 + 2줄 텍스트 + 화살표, `:active`에 grey100 |
| **면 구분** | 보더 없이 `--grey-background` + 여백으로 섹션 분리 |

### Step 4 — 동적 타입 검증

브라우저 글자 크기 200%로 올려 레이아웃 깨짐을 확인한다.

### 신규 컴포넌트 결정 트리

```
새 컴포넌트가 필요하다
  → 기존 40여 종 카탈로그의 조합으로 가능한가?
    yes → 조합 사용 (ListRow + Badge 등)
    no  → 시맨틱 토큰만으로 색을 구성할 수 있는가?
      yes → 추가하되 라운드·터치 타깃·동적 타입 규칙 상속
      no  → 원시 스케일에 색을 추가하는 것부터 재검토
```
