# Toast 작업 결과를 잠깐 알려주고 스스로 사라지는 알림입니다. > 원본: `03_ASSET_GUIDE/catalog/toast.js` · CSS: `03_ASSET_GUIDE/styles/elements/toast.css` > kind `component` · category `overlay` · status `stable` · specSource `measured` --- ## 1. 구성 어두운 칩 하나입니다. **Message** 만 필수이고 앞의 **Icon**, 뒤의 **Action** 과 **Close** 는 선택입니다. 앱의 어느 화면 위에서도 대비를 확보하도록 칩은 두 테마 모두 어둡게 유지합니다. ``` ┌────────────────────────────────────────────┐ │ ⊗ 파일을 올리지 못했어요 다시 시도 ✕ │ └────────────────────────────────────────────┘ ①② ③ ④ ⑤ ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | ① | Container | 필수 | `atom-toast` | `--atom-bg-tooltip` 칩. 폭 340px, 최소 높이 44px. | | ② | Icon | tone 을 쓰면 필수 | `atom-toast__icon` | tone 을 쓰면 필수입니다. 색만으로 성공·실패를 구분하지 않기 위한 요소입니다. | | ③ | Message | 필수 | `atom-toast__message` | 알림 문구. 두 줄까지 늘어나고 그 뒤로는 잘립니다. | | ④ | Action | 선택 | `atom-toast__action` | 선택. 하나만 둡니다. 있으면 자동 소멸을 끕니다. | | ⑤ | Close | 자동 소멸이 없으면 필수 | `atom-toast__close` | 자동으로 사라지지 않는 Toast 에는 필수입니다. | | — | Group | 필수(띄울 때) | `atom-toast-group` | live region 이자 위치를 정하는 컨테이너. Toast 보다 먼저 페이지에 존재해야 합니다. | --- ## 2. 종류 / 크기 ### 치수 | 대상 | 값 | | --- | --- | | 폭 | 340px · `max-width: 100%` 로 좁은 화면에서 줄어듭니다 | | 최소 높이 | 44px | | 패딩 | 13px 16px (`--atom-x4`) | | 모서리 | `--atom-radius-r2` (8px) | | 폰트 | 13px / line-height 1.45 | | Icon ↔ Message 간격 | `--atom-x2` (8px) | | Message ↔ Action 간격 | 16px (`--atom-x2` gap + Action 의 8px 마진) | | Action | 13px / 700 · `--atom-fg-tooltip` · underline | | 그림자 | `--atom-shadow-s2` | 폭은 340px 이지만 `max-width: 100%` 가 함께 있어 좁은 화면에서는 컨테이너 폭까지 줄어듭니다. 좌우 여백은 `atom-toast-group` 의 `padding-inline: var(--atom-x4)` 가 만듭니다. 최소 높이 44px 은 Action 의 터치 타깃에서 나온 값입니다. Action 과 Close 는 `align-self: stretch` 와 음수 블록 마진으로 칩 높이를 그대로 채우므로, 글자 높이(19px)가 아니라 44px 이 눌리는 영역입니다. 컨테이너 `gap` 이 8px 인 것은 Icon 을 Message 에 붙이기 위한 것입니다. 양쪽 모두 16px 이면 아이콘이 Message 와 떨어진 별개 항목처럼 보입니다. Message ↔ Action 의 16px 는 Action 의 `margin-inline-start` 8px 가 되돌려 줍니다. ### Tone `neutral` · `positive` · `critical` 세 가지입니다. tone 은 **배경이 아니라 아이콘 색**을 바꿉니다. 칩은 세 tone 모두 `--atom-bg-tooltip` 을 그대로 씁니다. | Tone | 아이콘 | 아이콘 색 | | --- | --- | --- | | `neutral` (기본) | `#atom-i-info` (선택) | `--atom-fg-tooltip` | | `positive` | `#atom-i-check-circle` | `--atom-fg-positive` | | `critical` | `#atom-i-x-circle` | `--atom-fg-critical` | tone 마다 배경색을 두면 라이트·다크 각각 세 쌍, 여섯 조합의 대비를 따로 맞춰야 합니다. 그래서 칩은 어두운 색 하나로 고정하고 **아이콘 모양**으로 tone 을 구분합니다. 색각 이상 사용자에게도 같은 정보가 전달됩니다. seed 는 배경색을 나누지만, ATOM 은 Toast 가 앱의 어느 화면 위에도 뜬다는 점을 우선했습니다. > [!WARNING] > **tone 을 쓰면 아이콘은 필수입니다** > `--tone_positive` · `--tone_critical` 은 아이콘 색만 바꿉니다. 아이콘 없이 tone 만 > 지정하면 화면에 아무 차이도 나타나지 않습니다. ### Action 오른쪽 끝에 텍스트 버튼 하나를 둘 수 있습니다. Action 이 있으면 자동 소멸을 끄고 `__close` 를 함께 둡니다. 이유는 [접근성](#6-접근성) 을 보세요. Action 색은 `--atom-fg-tooltip` 입니다. 밝은 브랜드색을 쓸 수 없어서 굵기(700)와 밑줄, 위치로 버튼임을 알립니다. > [!NOTE] > **브랜드색을 쓰지 않는 이유** > `--atom-bg-tooltip` 은 두 테마 모두 어둡습니다. 그래서 테마에 따라 밝기가 뒤집히는 > 토큰을 여기에 쓸 수 없습니다. `--atom-primary-500` 은 라이트 3.4:1 · 다크 4.2:1 로 > 13px 텍스트 기준 4.5:1 에 미달하고, `--atom-primary-200` 은 라이트에서 10.3:1 이지만 > 다크에서 `#202564` 로 어두워져 1.25:1 이 됩니다. 두 테마 모두 밝게 남는 토큰은 > `--atom-fg-tooltip` 하나뿐입니다. ### 지속 시간 CSS 에는 노출 시간이 없습니다. 스크립트가 정할 값이고, 레퍼런스가 크게 갈립니다. | 시스템 | 값 | | --- | --- | | Codeit Toast | 2~3초 | | Ant Message | 3초 | | seed Snackbar | **4초** (긴 문구는 연장, 사용자가 만지면 타이머 정지) | | Ant Notification | 4.5초 | | Spectrum | **자동 소멸 기본 꺼짐**, 켤 경우 최소 5초 | > [!NOTE] > **ATOM 의 값** > **액션이 없으면 4초, 액션이 있으면 자동으로 사라지지 않습니다.** 4초는 한국어 한 > 줄(약 20자)을 읽고 판단할 수 있는 최소 시간이고, seed 와 같은 값입니다. 마우스를 > 올리거나 포커스가 들어오면 타이머를 멈춥니다. ### 위치 기본은 화면 아래 중앙입니다. 위치는 개별 Toast 가 아니라 `atom-toast-group` 이 결정합니다. 화면에 붙일 때는 `--anchor_screen`(fixed), 폰 프레임이나 특정 영역 안에 둘 때는 `--anchor_parent`(absolute) 를 씁니다. | 모디파이어 | 결과 | | --- | --- | | `--placement_bottom` | 기본. 아래에서 `--atom-x4`(16px) | | `--placement_top` | 위에서 `--atom-x4`(16px) | | `--offset_raised` | 아래에서 `--atom-x14`(56px). 하단 내비게이션이나 FAB 위로 올립니다 | 그룹은 화면 폭을 가로지르므로 `pointer-events: none` 을 두고 Toast 에만 `auto` 를 되돌려 줍니다. 그렇지 않으면 화면 아래쪽 전체가 클릭을 가로챕니다. `z-index` 는 80 으로, 셸(최대 60)과 Dialog 보다 위입니다. Dialog 안에서 일어난 일을 알리는 Toast 가 Dialog 에 가려지지 않게 하기 위해서입니다. 플로팅 액션 버튼(`fab`)이나 하단 내비게이션(`bottom-navigation`)이 있으면 `--offset_raised` 로 그 위에 놓아 가리지 않게 합니다. ### 여러 개가 쌓일 때 새 Toast 는 언제나 그룹의 마지막에 붙습니다. `bottom` 배치는 위로, `top` 배치는 아래로 자라고, 어느 쪽이든 **가장 최근 것이 붙어 있는 화면 가장자리에 가장 가깝습니다.** 권장은 **한 번에 하나**입니다. 여러 개가 겹치면 무엇을 읽어야 할지 알 수 없고, live region 이 연달아 읽혀 소음이 됩니다. CSS 는 4개째부터 `display: none`(`:nth-last-child(n + 4)`)으로 가립니다. 이는 Toast 가 화면을 덮지 않게 하는 안전장치이며, 순서 관리는 스크립트가 맡습니다. ### 문구 길이 Message 는 기본으로 **두 줄까지** 늘어나고 그 뒤로는 잘립니다 (`-webkit-line-clamp: 2`). 한 줄로 고정하려면 `--lines_1` 을 쓰면 말줄임으로 처리됩니다. 한국어 줄바꿈은 `word-break: keep-all` 로 단어 단위입니다. 두 줄을 넘겨 잘리면 **문구를 줄입니다.** 칩이 커지면 아래 화면을 가리므로 높이를 늘리지 않고 자릅니다. ### 모션 진입은 8px 이동 + 페이드(`--atom-duration-base` · `--atom-ease-out`), 퇴장은 페이드만 (`--atom-duration-fast`)입니다. 이동 방향은 배치를 따릅니다. `bottom` 은 아래에서, `top` 은 위에서 들어옵니다. - 모션은 `[data-state="entering"]` · `[data-state="exiting"]` 에만 걸립니다. 카탈로그 카드나 문서 스테이지에 정적으로 놓인 Toast 가 렌더될 때마다 진입 애니메이션을 다시 재생하지 않게 하기 위한 것입니다. - `prefers-reduced-motion: reduce` 에서는 이동을 없애고 페이드만 남깁니다. `base.css` 가 모든 애니메이션을 0.01ms 로 줄이므로 8px 이동이 순간적으로 튀어 보이기 때문입니다. - 그때도 `animationend` 는 발생합니다. 퇴장 애니메이션에 정리(DOM 제거)를 걸어 둔 스크립트가 동작하도록 `animation: none` 을 쓰지 않습니다. ### 클래스 계약 `styles/elements/toast.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다. | 대상 | Prop | 값 | 클래스 | |---|---|---|---| | `atom-toast` | `tone` | `neutral` · `positive` · `critical` | — · `--tone_positive` · `--tone_critical` | | `atom-toast` | `singleLine` | `false` · `true` | — · `--lines_1` | | `atom-toast-group` | `placement` | `bottom` · `top` | — · `--placement_top` | | `atom-toast-group` | (구현 `anchor`) | 없음 · `screen` · `parent` | — · `--anchor_screen` · `--anchor_parent` | | `atom-toast-group` | (구현 `raised`) | `false` · `true` | — · `--offset_raised` | 상태 선택자: `atom-toast[data-state="entering\|exiting"]` · `__action` / `__close` 의 `:hover` · `:focus-visible` · `[disabled]` --- ## 3. 상태 | State | 표현 | 선택자 | | --- | --- | --- | | (속성 없음) | 정적 표시. 모션 없음 | — | | entering | 8px 이동 + 페이드 인 (`--atom-duration-base`) · `top` 은 위에서 | `[data-state="entering"]` | | exiting | 페이드 아웃 (`--atom-duration-fast`) | `[data-state="exiting"]` | | Action · Close hover | `opacity: 1` · 밑줄 2px | `:hover` | | Action · Close focus | 2px `--atom-fg-tooltip` 링, offset `-2px` | `:focus-visible` | | Action · Close disabled | `opacity: 0.4` · `not-allowed` | `[disabled]` | --- ## 4. Props | Prop | Type | Default | 필수 | 설명 | |---|---|---|---|---| | `message` | `ReactNode` | | ● | 알림 문구입니다. 두 줄까지 보이고 그 뒤로는 잘립니다. | | `tone` | `"neutral" \| "positive" \| "critical"` | `"neutral"` | | 아이콘 색을 결정합니다. 배경은 바뀌지 않습니다. | | `icon` | `ReactNode` | | | 선행 아이콘입니다. `tone` 을 `neutral` 이 아닌 값으로 주면 필수입니다. | | `action` | `{ label: string, onClick: () => void }` | | | 액션 버튼입니다. 하나만. 주면 `duration` 이 자동으로 `null` 이 됩니다. | | `duration` | `number \| null` | `4000` | | 자동 소멸까지의 ms 입니다. `null` 이면 사라지지 않습니다. | | `dismissible` | `boolean` | 자동 | | 닫기 버튼입니다. `duration: null` 이면 필수입니다. | | `singleLine` | `boolean` | `false` | | `--lines_1` 을 붙여 한 줄 + 말줄임으로 고정합니다. | | `placement` | `"bottom" \| "top"` | `"bottom"` | | 그룹의 속성입니다. 개별 Toast 가 아니라 컨테이너가 위치를 정합니다. | ### Parts (compound) | Part | 요소 | 설명 | |---|---|---| | `Toast` | `
` | 칩 하나. `placement` 를 뺀 위 Props 와 `onDismiss` 를 받습니다. 자동 소멸 타이머는 hover · focus 동안 멈춥니다. | | `Toast.Group` | `
` | live region · 위치 컨테이너. `placement` 와 구현의 `anchor` · `raised` 를 받습니다. | | `Toast.Provider` | `Toast.Group` (`anchor="screen"`) | 앱 루트에 한 번 둡니다. 그룹을 미리 렌더하고 `useToast()` 로 Toast 를 띄웁니다. 한 번에 3개까지 보이고 나머지는 대기합니다. | | `useToast()` | 훅 | `show(options) → id` · `dismiss(id)`. `options` 는 `Toast` 의 Props 와 같습니다. `Toast.Provider` 밖에서 쓰면 에러입니다. | --- ## 5. Component tokens | 대상 | 속성 | Token | Value | |---|---|---|---| | Container | background | `{color.bg.tooltip}` | 라이트 `#1a1b20` / 다크 `#3a3b45` | | Container | color | `{color.fg.tooltip}` | 라이트 `#ffffff` / 다크 `#f3f3f5` | | Container | radius | `{radius.r2}` | 8px | | Container | shadow | `{shadow.s2}` | | | Container | padding-inline | `{space.x4}` | 16px | | Container | gap | `{space.x2}` | 8px | | Icon (positive) | color | `{color.fg.positive}` | 라이트 `#12b76a` / 다크 `#34d391` | | Icon (critical) | color | `{color.fg.critical}` | 라이트 `#e5484d` / 다크 `#ff6369` | | Action | color | `{color.fg.tooltip}` | Container 와 같은 쌍 | | Group | gap | `{space.x2}` | 8px | | Group | inset | `{space.x4}` · `{space.x14}` | 16px · 56px(`--offset_raised`) | | Motion | duration | `{duration.base}` · `{duration.fast}` | 200ms 진입 · 120ms 퇴장 | | Motion | easing | `{easing.out}` | | 폭(340px), 세로 패딩(13px), 폰트(13px), `z-index`(80)는 토큰이 아닙니다. ```json { "toast": { "container": { "background": { "$type": "color", "$value": "{color.bg.tooltip}" }, "color": { "$type": "color", "$value": "{color.fg.tooltip}" }, "radius": { "$type": "dimension", "$value": "{radius.r2}" }, "shadow": { "$type": "shadow", "$value": "{shadow.s2}" }, "padding-inline": { "$type": "dimension", "$value": "{space.x4}" }, "gap": { "$type": "dimension", "$value": "{space.x2}" } }, "icon": { "$type": "color", "positive": { "$value": "{color.fg.positive}" }, "critical": { "$value": "{color.fg.critical}" } }, "action": { "$type": "color", "$value": "{color.fg.tooltip}" }, "group": { "gap": { "$type": "dimension", "$value": "{space.x2}" }, "inset": { "$type": "dimension", "$value": "{space.x4}" }, "inset-raised": { "$type": "dimension", "$value": "{space.x14}" } }, "motion": { "enter": { "$type": "duration", "$value": "{duration.base}" }, "exit": { "$type": "duration", "$value": "{duration.fast}" }, "easing": { "$type": "cubicBezier", "$value": "{easing.out}" } } } } ``` --- ## 6. 접근성 > [!NOTE] > 참고한 네 시스템 중 Toast 의 live region 처리를 문서화한 곳은 Ant 뿐이고 > (`role: 'alert' | 'status'` prop), 나머지는 없습니다. 아래는 ATOM 이 정한 것입니다. - **컨테이너가 live region 입니다** — `atom-toast-group` 에 `role="status"` + `aria-live="polite"` 를 두고 **내용만 갈아 넣습니다.** Toast 를 띄울 때 컨테이너째로 DOM 에 넣으면 스크린리더가 변화를 감지하지 못하는 경우가 있으므로, live region 은 개별 Toast 가 아닌 그룹에 둡니다. - **`assertive` 는 신중히** — `critical` tone 이라도 기본은 `polite` 입니다. 개별 Toast 에 `role="alert"` 를 주는 것은 사용자의 현재 작업을 끊어도 될 때만입니다. - **포커스를 옮기지 않습니다** — Toast 가 나타났다고 포커스를 가져가면 하던 입력이 끊깁니다. `aria-live` 로 읽히게만 합니다. - **액션이 있으면 자동 소멸을 끕니다** — 포커스를 옮기지 않으므로 액션 버튼은 탭 순서의 끝에 놓이고, 4초 안에 `Tab` 으로 도달하기 어렵습니다. - **타이머 정지** — 마우스를 올리거나 포커스가 들어오면 자동 소멸 타이머를 멈춥니다. 읽는 중에 사라지지 않아야 합니다. CSS 로는 표현할 수 없으므로 스크립트가 처리합니다. - **4초의 근거** — 한국어 한 줄(약 20자)을 읽고 판단할 수 있는 최소 시간입니다. 문구가 길면 늘립니다. - **한 번에 하나** — live region 이 여러 개면 스크린리더가 연달아 읽어 소음이 됩니다. 그룹은 하나만 둡니다. - **되돌릴 수 없는 정보를 Toast 에만 담지 않습니다** — 자동으로 사라지는 알림은 기록으로 남지 않습니다. - **색만으로 구분하지 않습니다** — tone 은 아이콘 색만 바꿉니다. `#atom-i-check-circle`(성공)과 `#atom-i-x-circle`(실패)이 모양으로 구분됩니다. - **대비** — Message 는 `--atom-fg-tooltip` on `--atom-bg-tooltip` 으로 17.19:1, Action 도 같은 쌍입니다. tone 아이콘은 `--atom-fg-positive` 6.6:1(라이트) · 5.8:1(다크), `--atom-fg-critical` 4.4:1(라이트) 로 비텍스트 3:1 기준을 넘습니다. - **포커스 표시** — 어두운 칩 위에서 브랜드색 링(`--atom-primary-600`)이 보이지 않으므로 Action 과 Close 는 `--atom-fg-tooltip` 링을 씁니다. 칩 가장자리까지 채우는 버튼이라 링은 `outline-offset: -2px` 로 안쪽에 그립니다. - **모션 감소** — 진입 이동을 없애고 페이드만 남깁니다. Toast 는 위치가 고정이라 모션이 없어도 정보 손실이 없습니다. --- ## 7. 사용 가이드 ### 놓쳐도 되는 것만 담습니다 Toast 는 자동으로 사라지므로 사용자가 보지 못할 수 있습니다. **놓치면 안 되는 정보는 Toast 에 담지 않습니다.** - ✅ **Do** "저장했어요" · "링크를 복사했어요" 처럼 놓쳐도 되는 결과만 담습니다. - ❌ **Don't** 결제 결과나 계정 삭제 결과를 Toast 로만 알리지 않습니다. ### 어디에 알릴지 고르기 | 상황 | 쓸 것 | | --- | --- | | 방금 한 작업의 결과를 짧게 알린다 | **Toast** | | 진행하기 전에 답이 필요하다 | [Dialog](./dialog.md) | | 특정 입력 칸의 문제를 알린다 | [Form Field](./field.md) 의 오류 메시지 | | 화면 전체에 계속 걸려 있어야 하는 경고 | Page Banner (`page-banner`) · Callout (`callout`) | | 여러 단계 안내나 긴 설명 | Callout (`callout`) · [Tooltip](./tooltip.md) | ### 오류를 Toast 로 알릴 것인가 레퍼런스가 갈리는 지점입니다. Spectrum 은 오류 Toast 를 적극 권하고 "결제 실패" 를 예로 듭니다. Codeit 은 결제 확인 같은 중요한 결정을 Toast 로 전달하지 말라고 합니다. **ATOM 의 제안: 사용자가 다시 시도할 수 있는 오류만 Toast 로 알리고, 액션과 닫기 버튼을 함께 둡니다.** - ✅ **Do** 다시 시도할 수 있는 오류에 `critical` tone · 액션 · 닫기를 함께 둡니다. - ❌ **Don't** 결제 실패처럼 되돌릴 수 없는 결과를 액션도 닫기도 없이 Toast 로만 알리지 않습니다. > [!CAUTION] > 오류 Toast 는 자동으로 사라지지 않게 둡니다. 오류가 사라지면 원인을 확인할 수 > 없습니다. ### 액션이 있으면 자동으로 사라지지 않습니다 4초 안에 버튼을 누르기는 마우스 사용자에게도 촉박하고, 키보드 사용자는 시간 안에 도달하기 어렵습니다. - ✅ **Do** 액션이 있으면 닫기 버튼을 함께 두고 자동 소멸을 끕니다. - ⚠️ **Caution** 자동 소멸을 유지해야 한다면 같은 액션을 화면 다른 곳에서도 할 수 있어야 합니다. - ❌ **Don't** 자동으로 사라지는 Toast 에 유일한 실행 경로를 두지 않습니다. ### 액션은 하나, 라벨은 구체적으로 "확인" 은 아무것도 하지 않는 버튼이고, "닫기" 는 `__close` 와 중복입니다. - ✅ **Do** "되돌리기" · "목록 보기" · "다시 시도" — 무엇이 일어나는지 라벨에 씁니다. - ❌ **Don't** "확인" · "닫기" · "취소" 를 액션 라벨로 쓰지 않습니다. ### 문구는 한 줄, 결과를 먼저 이미 일어난 일을 알리는 것이므로 완료형으로 씁니다. 한 문장이면 마침표를 쓰지 않습니다. 파일명이나 사용자 이름을 넣지 않습니다. 번역이 어려워지고, 그 맥락은 화면의 다른 곳에 이미 있습니다. - ✅ **Do** "저장했어요" — 결과 하나만, 한 줄로. - ❌ **Don't** "회원님께서 등록하신 항목이 정상적으로 저장 처리되었습니다." 처럼 두 줄을 넘기지 않습니다. - ❌ **Don't** "저장에 실패하지 않았어요" — 이중 부정을 쓰지 않습니다. ### 자주 띄우지 않습니다 알림이 잦으면 무시되기 쉽고, 화면 하단이 계속 가려집니다. 관련된 알림은 하나로 묶습니다. - ✅ **Do** "3개 항목을 삭제했어요" 로 묶습니다. - ❌ **Don't** 항목마다 Toast 를 띄우지 않습니다. ### 되돌리기에 Toast 를 씁니다 되돌릴 수 있는 삭제는 확인 Dialog 없이 바로 실행하고 "되돌리기" Toast 를 띄웁니다. 확인 단계가 줄고, 실수는 복구할 수 있습니다. - ✅ **Do** 되돌릴 수 있는 삭제는 Toast + "되돌리기" 로 처리합니다. - ⚠️ **Caution** 되돌릴 수 없는 삭제는 [Dialog](./dialog.md) 로 먼저 확인받습니다. ### 관련 컴포넌트 - [Form Field](./field.md) — 특정 입력 칸의 오류는 칸 아래에 붙입니다. - [Badge](./badge.md) — 사라지지 않고 상태를 계속 보여줘야 할 때. --- ## 8. 예시 ```tsx // 앱 루트에 한 번 — 그룹(live region)이 Toast 보다 먼저 존재합니다. // 어디서든 const toast = useToast(); toast.show({ message: "링크를 복사했어요" }); // 4초 후 자동 소멸 toast.show({ message: "항목을 삭제했어요", action: { label: "되돌리기", onClick: undo } }); // 자동 소멸 없음 + 닫기 toast.show({ tone: "critical", icon: , message: "파일을 올리지 못했어요", action: { label: "다시 시도", onClick: retry } }); // 정적 표시 (문서 스테이지 · 특정 영역) ``` ```html
링크를 복사했어요
항목을 삭제했어요
파일을 올리지 못했어요
```