# UI Design System — Material Design 3

> 어떤 스택에서든 동일한 톤을 이식하기 위한 단일 소스. Google Material Design 3(material.io)의 공개 토큰 체계를 웹 이식용으로 재해석한 문서다.
>
> **한 줄 정의**: 소스 컬러 하나에서 **톤 팔레트(0~100)** 를 뽑고, 거기서 `primary / on-primary / primary-container / on-primary-container` 같은 **역할(role) 토큰**을 파생시키는 시스템. 기준 팔레트의 퍼플(`#6750a4`)은 예시일 뿐 — 구조가 브랜드보다 우선한다(Material You).

---

## 목차

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

---

## 1. 디자인 원칙

| 원칙 | 정의 |
|---|---|
| **톤 팔레트 파생** | 모든 색은 소스 컬러의 톤 스케일(0=검정 ~ 100=흰색)에서 나온다. 임의 hex 추가 금지. |
| **역할 4종 세트** | 색 역할은 `X / on-X / X-container / on-X-container` 4종 묶음으로만 존재 — 대비가 구조적으로 보장된다. |
| **표면 = surface + tint** | 엘리베이션이 높을수록 surface에 primary 틴트가 섞인다. 그림자보다 틴트가 계층의 주 신호. |
| **다이내믹 컬러** | 사용자 배경화면·브랜드 소스에서 팔레트가 재생성돼도 UI가 성립해야 한다 — 역할 토큰만 참조하면 자동. |
| **형태 스케일** | 라운드도 토큰(4/8/12/16/28px…)이다. 컴포넌트별 임의 곡률 금지, FAB·버튼은 큰 라운드. |
| **상태 레이어** | hover/press는 색 교체가 아니라 전경색 8%/12% 오버레이 — 모든 컴포넌트에 동일 규칙. |

---

## 2. 색상 토큰

### 2-1. 역할 토큰 (라이트, 기준 팔레트)

```css
:root {
  --primary:              #6750a4;
  --on-primary:           #ffffff;
  --primary-container:    #eaddff;
  --on-primary-container: #21005d;

  --secondary:            #625b71;
  --on-secondary:         #ffffff;
  --secondary-container:  #e8def8;
  --on-secondary-container:#1d192b;

  --tertiary:             #7d5260;
  --tertiary-container:   #ffd8e4;

  --error:                #b3261e;
  --on-error:             #ffffff;
  --error-container:      #f9dedc;
  --on-error-container:   #410e0b;

  --surface:              #fffbfe;
  --on-surface:           #1c1b1f;
  --surface-variant:      #e7e0ec;
  --on-surface-variant:   #49454f;
  --surface-container:    #f3edf7;  /* 카드·시트 기본 표면 */
  --surface-container-high:#ece6f0;

  --outline:              #79747e;
  --outline-variant:      #cac4d0;

  --inverse-surface:      #313033;  /* 스낵바 */
  --inverse-on-surface:   #f4eff4;
}
```

### 2-2. 사용 규칙

| 역할 | 토큰 |
|---|---|
| 본문·제목 | `var(--on-surface)` |
| 보조 텍스트 | `var(--on-surface-variant)` |
| 페이지 배경 | `var(--surface)` |
| 카드·시트 | `var(--surface-container)` |
| 채움 버튼·FAB | `var(--primary)` + `var(--on-primary)` |
| 강조 옅은 면 (칩·선택) | `var(--primary-container)` + `var(--on-primary-container)` |
| 입력 테두리 | `var(--outline)` |
| 구분선 | `var(--outline-variant)` |
| 스낵바 | `var(--inverse-surface)` + `var(--inverse-on-surface)` |

> **핵심 규칙**: `X` 위에는 반드시 `on-X`. container 위에는 on-container. 이 짝을 깨는 순간 접근성 보장이 사라진다.

---

## 3. 타이포그래피

### 3-1. 폰트

```css
:root {
  --font-sans: Roboto, 'Noto Sans KR', system-ui, sans-serif;
}

body { font-family: var(--font-sans); color: var(--on-surface); background: var(--surface); }
```

```html
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Noto+Sans+KR:wght@400;500;700&display=swap" />
```

### 3-2. 타입 스케일 — 5역할 × 3크기

| 토큰 | `font-size` / `line-height` | `font-weight` |
|---|---|---|
| display-large | 57 / 64px | 400 |
| display-medium | 45 / 52px | 400 |
| display-small | 36 / 44px | 400 |
| headline-large | 32 / 40px | 400 |
| headline-medium | 28 / 36px | 400 |
| headline-small | 24 / 32px | 400 |
| title-large | 22 / 28px | 400 |
| title-medium | 16 / 24px | 500 |
| title-small | 14 / 20px | 500 |
| body-large | 16 / 24px | 400 |
| body-medium | 14 / 20px | 400 |
| body-small | 12 / 16px | 400 |
| label-large | 14 / 20px | 500 |
| label-medium | 12 / 16px | 500 |
| label-small | 11 / 16px | 500 |

- Display·Headline이 **Regular(400)** — 큰 글자는 무게가 아닌 크기로 말한다.
- 버튼 라벨은 `label-large`(14/500) 고정.

---

## 4. 형태 & 엘리베이션

### 4-1. 형태 스케일

```css
:root {
  --shape-xs:   0.25rem;   /* 4px  — 작은 칩 */
  --shape-sm:   0.5rem;    /* 8px  — 칩·메뉴 */
  --shape-md:   0.75rem;   /* 12px — 카드 */
  --shape-lg:   1rem;      /* 16px — 시트·다이얼로그 하위 */
  --shape-xl:   1.75rem;   /* 28px — 다이얼로그·검색바 */
  --shape-full: 62.4375rem; /* 버튼·FAB pill */
}
```

### 4-2. 엘리베이션 — 틴트 + 그림자

| 레벨 | 표면 | 그림자 |
|---|---|---|
| 0 | `--surface` | 없음 |
| 1 | `--surface-container` | `0 1px 2px rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)` |
| 2 | `--surface-container` | `0 1px 2px rgba(0,0,0,.3), 0 2px 6px 2px rgba(0,0,0,.15)` |
| 3 | `--surface-container-high` | `0 4px 8px 3px rgba(0,0,0,.15), 0 1px 3px rgba(0,0,0,.3)` |

---

## 5. 핵심 컴포넌트

### 5-1. 버튼 5종

```css
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.5rem;
  padding: 0 1.5rem;
  border: none;
  border-radius: var(--shape-full);   /* pill이 기본 */
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.btn--filled   { background: var(--primary); color: var(--on-primary); }
.btn--tonal    { background: var(--secondary-container); color: var(--on-secondary-container); }
.btn--outlined { background: transparent; border: 1px solid var(--outline); color: var(--primary); }
.btn--text     { background: transparent; color: var(--primary); padding: 0 0.75rem; }
.btn--elevated { background: var(--surface-container); color: var(--primary);
                 box-shadow: 0 1px 2px rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15); }

/* 상태 레이어 — 색 교체가 아니라 오버레이 */
.btn::after {
  content: '';
  position: absolute; inset: 0;
  background: currentColor;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.btn:hover::after  { opacity: 0.08; }
.btn:active::after { opacity: 0.12; }
```

### 5-2. 카드

```css
.card {
  background: var(--surface-container);
  border-radius: var(--shape-md);
  padding: 1rem;
}
.card--outlined { background: var(--surface); border: 1px solid var(--outline-variant); }
```

### 5-3. FAB

```css
.fab {
  width: 3.5rem; height: 3.5rem;
  border: none;
  border-radius: var(--shape-lg);
  background: var(--primary-container);
  color: var(--on-primary-container);
  box-shadow: 0 4px 8px 3px rgba(0,0,0,.15);
  cursor: pointer;
}
```

### 5-4. 텍스트 필드 (Outlined)

```css
.field { position: relative; }
.field__input {
  width: 100%;
  padding: 1rem;
  border: 1px solid var(--outline);
  border-radius: var(--shape-xs);
  background: transparent;
  font-size: 1rem;
  color: var(--on-surface);
}
.field__input:focus {
  outline: none;
  border: 2px solid var(--primary);
  padding: calc(1rem - 1px);
}
.field__label {
  position: absolute;
  top: -0.5rem; left: 0.75rem;
  padding: 0 0.25rem;
  background: var(--surface);
  font-size: 0.75rem;
  color: var(--on-surface-variant);
}
.field__input[aria-invalid="true"] { border-color: var(--error); }
```

### 5-5. 스낵바

```css
.snackbar {
  background: var(--inverse-surface);
  color: var(--inverse-on-surface);
  border-radius: var(--shape-xs);
  padding: 0.875rem 1rem;
  box-shadow: 0 4px 8px 3px rgba(0,0,0,.15);
}
.snackbar__action { color: #d0bcff; font-weight: 500; }  /* inverse-primary */
```

---

## 6. 인터랙션

- **상태 레이어**: hover 8% / focus 12% / press 12% — 전경색 오버레이로 통일 (§5-1).
- 리플(ripple)은 선택 사항 — 웹에서는 상태 레이어만으로 충분.
- 모션: emphasized easing `cubic-bezier(0.2, 0, 0, 1)`, 지속 0.2~0.5s. 공간 이동은 컨테이너 변형(container transform) 감각.
- `prefers-reduced-motion` 시 변형 모션 제거, 페이드만 유지.

---

## 7. 다크 모드

같은 톤 팔레트에서 **어두운 톤 구간을 선택**한다 — 명도 반전이 아니다.

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

  --primary:              #d0bcff;   /* 톤 80 */
  --on-primary:           #381e72;   /* 톤 20 */
  --primary-container:    #4f378b;   /* 톤 30 */
  --on-primary-container: #eaddff;   /* 톤 90 */

  --secondary:            #ccc2dc;
  --secondary-container:  #4a4458;
  --on-secondary-container:#e8def8;

  --error:                #f2b8b5;
  --error-container:      #8c1d18;

  --surface:              #141218;
  --on-surface:           #e6e0e9;
  --surface-variant:      #49454f;
  --on-surface-variant:   #cac4d0;
  --surface-container:    #211f26;
  --surface-container-high:#2b2930;

  --outline:              #938f99;
  --outline-variant:      #49454f;

  --inverse-surface:      #e6e0e9;
  --inverse-on-surface:   #313033;
}
```

- 다크에서 primary가 **밝은 톤 80**으로 바뀌고 on-primary가 어두워진다 — 역할 짝은 그대로 유지된다.

---

## 8. 접근성

- 역할 짝(`X`/`on-X`)을 지키면 4.5:1 이상 대비가 톤 시스템에 의해 보장된다.
- 터치 타깃 최소 48×48dp — 40px 버튼도 주변 8px을 히트 영역에 포함.
- 상태 레이어는 색약 사용자를 위해 커서·포커스 링과 병행: `outline: 2px solid var(--primary)`.
- 텍스트 필드 라벨은 플레이스홀더로 대체 금지 — 떠 있는 라벨 유지.
- 스낵바는 `role="status"`, 오류 다이얼로그는 `role="alertdialog"`.

---

## 9. 안티패턴

| 금지 | 이유 |
|---|---|
| 톤 팔레트 밖 임의 hex | 다이내믹 컬러·다크 파생 붕괴 |
| `X` 위에 on-X 아닌 색 | 대비 보장 구조 파괴 |
| hover를 색 교체로 구현 | 상태 레이어(8/12%) 규칙 위반 |
| 그림자만으로 엘리베이션 | 표면 틴트가 주 신호 |
| 형태 스케일 밖 임의 라운드 | 형태도 토큰이다 |
| Display/Headline에 Bold | 큰 글자는 Regular — 크기가 말한다 |
| 다크 모드를 명도 반전으로 | 톤 구간 재선택 (primary 40→80) |
| 버튼 라벨 임의 크기 | label-large(14/500) 고정 |

---

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

### 10-1. Tailwind CSS v4

```css
@import "tailwindcss";

@theme {
  --color-primary:              #6750a4;
  --color-on-primary:           #ffffff;
  --color-primary-container:    #eaddff;
  --color-on-primary-container: #21005d;
  --color-surface:              #fffbfe;
  --color-on-surface:           #1c1b1f;
  --color-surface-container:    #f3edf7;
  --color-on-surface-variant:   #49454f;
  --color-outline:              #79747e;
  --color-outline-variant:      #cac4d0;
  --color-error:                #b3261e;
}

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

### 10-2. Tailwind CSS v3

```js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        primary:      'var(--primary)',
        'on-primary': 'var(--on-primary)',
        'primary-container': 'var(--primary-container)',
        surface:      'var(--surface)',
        'on-surface': 'var(--on-surface)',
        'surface-container': 'var(--surface-container)',
        outline:      'var(--outline)',
        error:        'var(--error)',
      },
      borderRadius: { xs: '0.25rem', md: '0.75rem', xl: '1.75rem' },
    },
  },
};
```

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

공식 구현(Material Web, MUI, Angular Material)을 쓰더라도 토큰 오버라이드는 CSS 변수 층에서 — 컴포넌트 스타일 직접 수정 금지.

---

## 11. 이식 가이드

### Step 1 — 역할 토큰 복사

§2 라이트 + §7 다크 블록을 전역 CSS에 붙인다. 브랜드 소스 컬러가 있으면 [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)로 같은 구조의 팔레트를 생성해 교체한다.

### Step 2 — 폰트 연결

Roboto + Noto Sans KR 링크를 추가한다.

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

| 패턴 | 핵심 CSS |
|---|---|
| **pill 버튼 + 상태 레이어** | `border-radius: full` + `::after` 오버레이 8/12% |
| **container 짝** | 옅은 면은 `X-container` + `on-X-container`만 |
| **틴트 엘리베이션** | 계층 = surface-container 단계 + 얕은 그림자 |

### Step 4 — 역할 짝 검증

모든 배경/전경 조합이 `X`/`on-X` 짝인지 검사한다 — 아니면 접근성 보장이 없다.

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

```
새 컴포넌트가 필요하다
  → 필요한 색이 기존 역할로 표현되는가?
    yes → 역할 토큰 사용
    no  → 새 역할이 아니라 새 "소스 컬러"가 필요한가부터 재검토
  → 라운드는 형태 스케일(xs~full) 중 선택
  → hover/press는 상태 레이어 규칙 상속
```
