Snackbar
화면 아래에 잠깐 떴다 사라지는 띠(토스트) — 방금 한 일의 결과("거래를 저장했어요."), 뒤에서 끝난 일, 다시 하면 되는 가벼운 실패를 알린다. 조치해야 끝나는 오류 · 계속 보여야 할 경고 · 결정은 여기 두지 않는다 — 그 자리의 Callout · 페이지의 Page Banner · Result Section · Alert Dialog 다.
구조는 당근 SEED Snackbar(Apache-2.0)를 따른다 — 짙은 띠 · 아이콘(성공 · 실패에만) · 글 · 액션 하나, 화면 아래 가운데에 한 번에 하나. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-02 사용자 결정). 옛 Sonner(토스트) 스펙을 대신한다.
수치 원본은 snackbar.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
톤 · 액션 · 글 길이를 고르고 띄우면 스펙대로 그린 띠가 아래에서 뜨고, 시간이 지나면 사라진다. 마우스를 올리거나 키보드로 옮기면 멈춘다.
import { useSnackbar } from "@/components/ui/snackbar"
const snackbar = useSnackbar()
snackbar.show({
message: "거래를 삭제했어요.",
action: { label: "되돌리기", onClick: undo },
})Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Region | 자리 — 화면 아래 가운데. 탭 바 · 플로팅 버튼 · 바닥 버튼 위 8. |
| ⓑ Container | 띠 — 짙은 바탕(다크는 밝은 띠) · 최대 464. |
| ⓒ Icon | 아이콘 — 성공(체크) · 실패(느낌표)에만. |
| ⓓ Message | 글 — 무엇이 됐는지 먼저. 줄이지 않는다. |
| ⓔ Action | 액션 — 하나, 동작 이름("되돌리기"). 누르면 닫힌다. |
부위
| 부위 | 설명 |
|---|---|
region | 자리 — 화면 아래 가운데. 탭 바 · 플로팅 버튼 · 바닥 버튼을 피해 그 위에 선다 |
root | 띠 — 짙은 바탕(다크는 밝은 띠), 한 번에 하나 |
icon | 아이콘 — 성공 · 실패에만 |
content | 글과 액션 — 양 끝에 놓는다 |
message | 글 — 무엇이 됐는지 먼저, 줄이지 않는다 |
action | 액션 — 하나, 동작 이름(되돌리기 · 잔액 고치기) |
closeButton | 보조 기술용 닫기 — 보이지 않고, 키보드 초점이 오면 띠 오른쪽 끝에 보인다 |
focusRing | 키보드 포커스 링(웹) |
Properties
띠
최소 44 · 여백 10 + 글 좌우 6(글은 띠 가장자리에서 16) · 모서리 8 · 그림자 없음. 글 14 / 19, 아이콘 24, 액션 14 · 700. 폭은 화면에서 좌우 8 을 뺀 만큼이고 최대 464 — 넓은 화면에서는 가운데에 선다. 글이 길면 줄을 바꾸고 자르지 않는다 — 두 줄 안에서 끝나게 쓴다.
- 최소 44 · 여백 10 + 글 좌우 6 · 모서리 8 · 그림자 없음
- 글 14 / 19 · 400 · 액션 14 · 700 · 글과 액션은 양 끝(사이 적어도 10)
- 폭은 자리 폭(화면 − 좌우 8 × 2), 최대 464
공통
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
region | 좌우 여백 | spacing-x2 8px | 화면 끝과 8 — 안전 영역이 있으면 그만큼 더 |
region | 아래 여백 | spacing-x2 8px | 아래 8 + 안전 영역 — 탭 바 · 플로팅 버튼 · 바닥 버튼이 있으면 그 위 8(안전 영역과 둘 중 큰 쪽 — 탭 바는 안전 영역을 품고 있다). 화면 키보드가 올라와 있으면 키보드 바로 위 8 — 웹 폰 브라우저도 보이는 화면의 아래 끝을 잰다(2026-10-15 사용자 결정 — 구현 세션 질문 Q-003-1) |
region | keyboardGap | spacing-x2 8px | 화면 키보드 위 끝 ↔ 띠 아래 끝 — 키보드도 피할 자리 하나다(안전 영역 · 탭 바 · 바닥 버튼과 넷 중 가장 위) |
region | offsetDuration | motion-duration-d4 200ms | 피할 자리(탭 바 · 바닥 버튼 · 키보드)가 바뀌어 띠가 옮겨 갈 때 — 모션 줄이기면 바로(SEED snackbar.region.offsetDuration) |
region | offsetEasing | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | SEED snackbar.region.offsetTimingFunction |
region | z-index | 400 | z-snackbar — specs/z-index.md L6. 시트 · 대화상자 · 확인창 위 |
root | 너비 | 100% | 자리 폭 — 최대 464, 넓으면 가운데 |
root | 최대 너비 | 464px | |
root | 최소 높이 | 44px | |
root | 여백 | spacing-x2_5 10px | |
root | 모서리 | radius-r2 8px | |
root | 배경 | color-bg-neutral-inverted #1A1F2E · 다크 #F5F6FA | 다크에서는 밝은 띠 |
root | 그림자 | none | 그림자 없이 면 색으로 뜬다(SEED · Elevation) |
root | 보이는 시간 | 4000ms | 액션이 있으면 6000ms. 마우스 · 손가락 · 키보드 초점이 머무는 동안 멈추고, 떠나면 처음부터 다시 센다 |
icon | 아이콘 | 24px | lucide 선 아이콘 — 체크(circle-check) · 느낌표(circle-alert). 오른쪽 여백 2 를 이 칸 안에 둬 그림은 22 다(SEED 와 같다 — 글은 띠 가장자리에서 40) |
icon | 오른쪽 여백 | spacing-x0_5 2px | 칸 안쪽 — 칸을 넓히지 않는다 |
content | 좌우 여백 | spacing-x1_5 6px | 글은 띠 가장자리에서 16(아이콘이 있으면 40) |
content | 간격 | spacing-x2_5 10px | 글과 액션은 양 끝 — 사이는 적어도 10 |
message | 글자 | text-t4 14px / 19px | 단어 단위 줄바꿈(v114) — 줄 수를 자르지 않는다(2026-09-22). 두 줄 안에서 끝나게 쓴다 |
message | 굵기 | 400 | |
message | 글자·아이콘 | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | |
action | 글자 | text-t4 14px / 19px | |
action | 굵기 | 700 | |
action | 글자·아이콘 | color-fg-brand-inverted HR #2ABE84 · Desk #7AA9F6 | 반전 짝(v115) — 짙은 띠 위 Desk 6.90 · HR 6.88, 다크의 밝은 띠 위 7.76 · 7.56 |
action | 터치 영역 | 글 + 좌우 8 × 44 | 띠 높이 안에서 위아래로 넓힌다 |
action | 모서리 | radius-r1 4px | 바탕이 없어 초점 링에만 보인다 |
closeButton | 크기 | 44px | 평소에는 보이지 않는다(보조 기술은 읽는다) — 키보드 초점이 오면 띠 오른쪽 끝에 X 16 으로 보인다 |
closeButton | 아이콘 크기 | 16px | |
closeButton | 색 | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | |
closeButton | 모서리 | radius-r2 8px | 바탕이 없어 초점 링에만 보인다 |
closeButton | 바깥 여백 | 위 · 아래 · 오른쪽 −10 | 띠 여백(10) 안으로 들어와 띠 높이를 44 로 둔다 |
Tone
neutral(기본)은 아이콘이 없다. 성공을 눈에 띄게 알릴 때만 positive(체크), 가볍게 실패했을 때 critical(느낌표) — 아이콘 색만 바뀐다. 짙은 띠 위에서 보이게 아이콘 · 액션은 반전 짝 색(fg-*-inverted)이다.
톤
| tone | icon 색 | 설명 |
|---|---|---|
neutral (기본) | — | 결과 · 안내 — 아이콘 없음 |
positive | color-fg-positive-inverted #25C062 · 다크 #167F3F — 반전 짝(v115) — 짙은 띠 위 6.87, 다크의 밝은 띠 위 4.70 | 성공을 눈에 띄게 알릴 때 — 체크 아이콘 |
critical | color-fg-critical-inverted #FF8477 · 다크 #D72323 — 반전 짝(v115) — 짙은 띠 위 6.89, 다크의 밝은 띠 위 4.68 | 가볍게 실패했을 때(다시 하면 되는 일) — 느낌표 아이콘 |
State
| 상태 | 모습 |
|---|---|
enabled | 보임 |
pressed | 액션 — 글만 2px 거리 축소 |
focused | 웹 — 키보드 포커스에만 띠 글자색 링. 띠 · 닫기는 안쪽 4, 액션은 바깥 2 — 어느 링이든 띠 위에 그린다(페이지 위로 나가면 보이지 않는다) |
상태
| 상태 | action 글자·아이콘 | action 배율 | action 축소 시간 | action 축소 곡선 | 링 두께 | 링 간격 | 링 색 | 액션 링 간격 |
|---|---|---|---|---|---|---|---|---|
enabled 보임 | color-fg-brand-inverted HR #2ABE84 · Desk #7AA9F6 — 반전 짝(v115) — 짙은 띠 위 Desk 6.90 · HR 6.88, 다크의 밝은 띠 위 7.76 · 7.56 | — | — | — | — | — | — | — |
pressed 액션 누름 — 2px 거리 축소 | color-fg-brand-inverted HR #2ABE84 · Desk #7AA9F6 — 반전 짝(v115) — 짙은 띠 위 Desk 6.90 · HR 6.88, 다크의 밝은 띠 위 7.76 · 7.56 | 2px 거리 축소 — 글만 준다 — 모션 줄이기면 하지 않는다 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | — | — | — | — |
focused 웹 — 키보드 포커스에만 링(띠 · 액션 · 닫기) | color-fg-brand-inverted HR #2ABE84 · Desk #7AA9F6 — 반전 짝(v115) — 짙은 띠 위 Desk 6.90 · HR 6.88, 다크의 밝은 띠 위 7.76 · 7.56 | — | — | — | 2px | -4px — 띠 · 닫기 — 안쪽 4. 링이 띠 글자색이라 바깥(페이지 위)에 그리면 보이지 않는다 | color-fg-neutral-inverted #FFFFFF · 다크 #242938 — 띠 글자색 — 브랜드 링은 짙은 띠 위에서 Desk 1.96 이라 3:1 에 못 미친다 | 2px — 액션 — 바깥 2. 띠 여백 안이라 띠 위에 그려진다 |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
나타남 | motion-duration-d3 150ms | motion-ease-enter cubic-bezier(0, 0, 0.15, 1) | scale 0.8 → 1, opacity 0 → 1 | 가운데를 기준점으로 — 모션 줄이기면 투명도만(v104) |
사라짐 | motion-duration-d2 100ms | motion-ease-exit cubic-bezier(0.35, 0, 1, 1) | scale → 0.8, opacity → 0 | |
새 것으로 바꿈 | motion-duration-d2 100ms | motion-ease-exit cubic-bezier(0.35, 0, 1, 1) | 지금 것 사라짐 → 새 것 나타남 | 한 번에 하나 — 새 것이 오면 지금 것을 바로 바꾼다. 지금 것에 액션이 있으면 끝날 때까지 지키고 새 것은 그 뒤에(기다리는 것은 하나 — 2026-10-22 1B) |
Guidelines
무엇으로 알리나 — 상황 → 고를 것
알리는 부품 다섯(Snackbar · Callout · Page Banner · Result Section · Alert Dialog)을 고르는 표는 여기 한 벌이다 — Callout · Page Banner · Help Bubble · Alert Dialog 문서는 자기 줄만 두고 이 표로 링크한다(2026-10-22 사용자 결정 3B — https://claude.ai/artifact/NuEPiLRH2fJidXdKzcDsCX). 나누는 법(일로 나눈다)은 2026-10-02 결정 그대로다.
먼저 묻는다 — 사용자가 지금 답해야 하나? 그렇다면 Alert Dialog. 아니면 얼마나 오래 보여야 하나(잠깐 · 조치할 때까지 · 상태가 풀릴 때까지)와 무엇에 걸린 일인가(방금 한 일 · 그 기능 · 페이지 전체 · 그 영역)로 고른다.
| 이런 일 | 쓰는 것 |
|---|---|
| 방금 한 일의 결과(저장 · 삭제 · 복사 · 옮김) | Snackbar — 되돌릴 수 있으면 "되돌리기" |
| 뒤에서 끝난 일(내보내기 · 동기화) | Snackbar |
| 다시 하면 되는 가벼운 실패(관심 종목에 넣지 못함) | Snackbar critical + "다시 시도"(켜고 끄는 단추의 실패는 되돌린 뒤 이 띠 — Toggle 20A) |
| 시트 · 대화상자가 열린 채 결과를 봐야 함(저장 실패 · 계산 결과) | 그 안 Callout — 폼 맨 위, 창은 연 채로 |
| 시트 · 대화상자를 닫으며 끝나는 일(저장하고 닫힘) | 닫힌 뒤 Snackbar |
| 입력값 하나가 틀림 | Field 의 오류 문구(칸 아래) |
| 저장 · 제출이 실패함 | 폼 맨 위 Callout critical — 폼은 연 채로 |
| 그 기능 · 내용 가까이의 팁 · 제약 · 주의 | Callout — 그 내용 바로 위 |
| 페이지 전체의 상태(연결 끊김 · 만료 예정 · 새 버전) | Page Banner — 머리 바로 아래, 한 화면에 하나 |
| 화면 · 영역이 비었음 · 불러오지 못함 · 끝남 | 그 자리 Result Section(못 불러왔으면 + "다시 시도") |
| 되돌릴 수 없는 결정 · 꼭 알아야 할 일 | Alert Dialog — 답할 때까지 닫히지 않는다 |
다섯 부품 견주기
| Snackbar | Callout | Page Banner | Result Section | Alert Dialog | |
|---|---|---|---|---|---|
| 알리는 것 | 방금 한 일의 결과 · 가벼운 실패 | 그 기능의 팁 · 제약 · 주의, 그 자리의 오류 | 페이지 전체의 상태 | 비어 있음 · 불러오기 실패 · 완료 | 되돌릴 수 없는 결정 · 꼭 알아야 할 일 |
| 자리 | 화면 아래 가운데(탭 바 · 키보드 위) | 본문 안, 그 내용 바로 위 · 폼 맨 위 | 머리 바로 아래 | 화면이나 그 영역 가운데 | 화면 가운데, 딤 위 |
| 폭 | 화면 폭에서 좌우를 남기고, 넓으면 최대 폭 | 콘텐츠 폭 | 화면 폭 전체 | 놓인 자리 | 확인창 최대 폭 |
| 범위 | 그 일 하나 | 그 기능 · 내용(로컬) | 페이지 전체(글로벌) | 그 화면 · 영역 | 그 일 하나 |
| 머무는 시간 | 잠깐 — 액션이 있으면 조금 더, 머무는 동안 멈춤 | 조치할 때까지 | 상태가 풀릴 때까지 | 상태가 바뀔 때까지 | 답할 때까지 |
| 수 | 한 번에 하나 | 필요한 만큼(쌓지 않는다) | 한 화면에 하나 | 영역마다 하나 | 하나 |
| 닫기 | 시간 · 액션 · 보조 기술용 닫기 | 한 번 보면 되는 안내만(경고 · 오류는 못 닫음) | 한 번 보면 되는 안내만(경고 · 오류는 못 닫음) | 없음(상태가 바뀌면 걷힘) | 버튼으로만 — 바깥은 무시 · Esc 는 취소 |
| 누를 것 | 액션 하나(동작 이름) | 링크 · 또는 상자 전체 | 버튼 하나 · 또는 띠 전체 | 해결 하나 + 보조 하나 | 버튼 둘(알릴 때 하나) |
| 스크롤 | 떠 있음 | 함께 움직임 | 붙일 수 있음(화면마다) | 함께 움직임 | 떠 있음(모달) |
| 사용자 응답 | 없어도 됨 | 없어도 됨 | 없어도 됨 | 없어도 됨(버튼은 고를 수 있음) | 꼭 답해야 함 |
수치(폭 · 시간 · 쌓임)는 각 부품의 YAML 이다 — 이 표는 고르는 법만 적는다. 입력칸의 오류(Field)와 몰라도 되는 설명(Help Bubble)은 이 표 밖 — 각 문서의 표가 맡는다.
총액이 바뀌어 분할 합계와 1,200원 달라요.
잠시 뒤 다시 시도해 주세요.
오류는 자리에서
모든 실패를 한곳에서 스낵바로 띄우지 않는다 — 서버가 보낸 글 · 영어 · 코드가 그대로 나가고, 같은 실패가 둘 · 넷씩 뜬다. 각 화면이 실패한 자리에서 알린다: 불러오기 실패는 Result Section, 저장 실패는 폼 맨 위 Callout, 칸 오류는 칸 아래. 스낵바는 그 자리에 둘 곳이 없는 가벼운 실패에만 쓴다.
잠시 뒤 다시 시도해 주세요.
자리 — 화면 아래 가운데
어느 폭이든 화면 아래 가운데다. 탭 바(Bottom Navigation) · 떠 있는 버튼(Floating Action Button) · 바닥 고정 버튼이 있으면 그 위 8 에 서고, 안전 영역(홈 표시줄)을 피한다. 화면 키보드가 올라와 있으면 키보드 바로 위 8 에 선다 — 탭 바 · 바닥 버튼이 키보드 뒤로 가도 띠는 보인다. 웹도 폰 브라우저에서 같다(키보드가 화면을 줄이지 않아도 보이는 화면의 아래 끝을 재어 피할 자리에 더한다). 2026-10-15 사용자 결정 — 구현 세션 질문 Q-003-1. 시트 · 대화상자가 열려 있어도 그 위에 뜬다(z L6). 다만 시트 · 대화상자에서 한 일은 창이 열린 채인지 닫히며 끝나는지로 가른다(2026-10-02 사용자 결정 그대로 — 2026-10-22 사용자 결정 2B 로 상황을 풀어 썼다, https://claude.ai/artifact/NuEPiLRH2fJidXdKzcDsCX).
| 시트 · 대화상자에서 한 일 | 알리는 자리 |
|---|---|
| 창이 열린 채 결과를 봐야 함(저장 실패 · 계산 결과) | 그 안 Callout — 폼 맨 위, 창은 연 채로 |
| 창을 닫으며 끝나는 일(저장하고 닫힘) | 닫힌 뒤 Snackbar |
| 칸 하나가 틀림 | 칸 아래 Field 오류 |
창이 열린 채 그 안의 결과를 스낵바로 띄우지 않는다 — 시트 위의 띠를 누르면 시트가 바깥 누름으로 닫히고 액션이 먹지 않는다.
시간 · 한 번에 하나
액션이 없으면 4초, 있으면 6초. 마우스를 올리거나, 손가락으로 누르고 있거나, 키보드 초점이 들어오면 멈추고, 떠나면 처음부터 다시 센다. 한 번에 하나만 — 새 띠가 오면 지금 띠를 바로 바꾼다(쌓지 않는다). 다만 지금 띠에 액션(되돌리기 · 다시 시도)이 있으면 그 띠는 끝날 때까지(시간 · 액션 · 닫기) 지키고, 새 띠는 그 뒤에 뜬다 — 그 경우만 줄을 세운다(SEED React strategy: "queued"). 기다리는 띠는 하나다 — 그 사이 또 오면 마지막 것만 남는다. 되돌리기 6초(사용자 결정)가 다음 결과에 잘리지 않고, 연달아 저장하는 흔한 경우(액션 없는 결과끼리)는 늦어지지 않는다(2026-10-22 사용자 결정 1B — https://claude.ai/artifact/NuEPiLRH2fJidXdKzcDsCX). 되돌리기 같은 액션은 스낵바에만 두지 않는다 — 시간이 지나면 사라지므로 같은 일을 할 다른 길(목록 · 상세)이 있어야 한다.
글
해요체 문장에 마침표(Writing v106) — 무엇이 됐는지 먼저: "거래를 저장했어요." · "거래를 삭제하지 못했어요."(액션 [다시 시도] 가 있으면 한 문장 — 버튼과 같은 말을 되풀이하지 않고, 폰에서 두 줄이 되지 않는다) · 액션이 없으면 "관심 종목에 넣지 못했어요. 잠시 뒤 다시 시도해주세요." 시스템 말("처리가 완료되었습니다") · "실패" 로 끝나는 말 · 서버가 보낸 글 · 영어 · 코드를 쓰지 않는다. 액션은 구체적인 동작 이름 한두 마디("되돌리기" · "잔액 고치기") — "확인" · "취소" 로 뭉뚱그리지 않는다.
큰 글자에서 글이 좁아지는 것은 이 쓰기 규칙으로 막는다 — 글은 두 줄 안, 액션은 한두 마디. 띠 모양은 SEED 와 같다(액션은 줄지 않고 글이 남은 폭에서 줄을 바꾼다). 규칙을 어긴 긴 글 · 긴 액션이 오면 200% 에서 글이 한두 글자씩 세로로 쌓여 띠가 화면을 덮는다 — 문구 검토에서 막는다. 영어 로케일은 액션이 길어지기 쉬우니("Try again" · "Fix balance") 번역 문구도 같은 규칙으로 본다(2026-10-17 사용자 결정 — 03-F 2B, https://claude.ai/artifact/F5SKUbGEWDG82F7B5RRjQB — 추천 2A "글 칸 반 폭 아래로 줄지 않음 · 액션 아래 줄" 이 아니다).
코드
레시피 recipes/shadcn/components/ui/snackbar.tsx 를 쓴다. 앱 맨 위에 SnackbarProvider 를 한 번 두고, 띄울 때는 useSnackbar() 의 show 를 부른다. 탭 바 · 바닥 버튼처럼 띠가 피해야 할 것은 SnackbarAvoidOverlap 으로 감싼다. 아래 미리보기는 스펙 값으로 그린 모습이다.
결과 알리기
저장하면 띠가 4초 동안 뜬다 — 마우스를 올리거나 Tab 으로 띠에 들어가면 멈추고, 떠나면 처음부터 센다.
import { useSnackbar } from "@/components/ui/snackbar"
const snackbar = useSnackbar()
await saveTransaction(draft)
snackbar.show({ message: "거래를 저장했어요." })되돌리기 — 액션이 있으면 6초
휴지통을 누르면 거래가 빠지고 띠가 6초 동안 뜬다(액션이 있으면 6초). 되돌리기를 누르면 그 자리로 돌아오고 띠는 닫힌다. 관심 종목 넣기가 실패하면 띠에 다시 시도가 달린다(6초).
snackbar.show({
message: "거래를 삭제했어요.",
action: { label: "되돌리기", onClick: () => restoreTransaction(id) },
})
// 가볍게 실패 — 다시 하면 되는 일만. 저장 · 불러오기 실패는 그 자리에서 알린다.
// 켜고 끄는 단추(관심 등록)는 되돌린 뒤 같은 요청을 다시 보내는 "다시 시도" 를 단다(Toggle 20A)
snackbar.show({
tone: "critical",
message: "관심 종목에 넣지 못했어요. 다시 시도해주세요.",
action: { label: "다시 시도", onClick: () => setWatched(stock.id, true) },
})피할 자리 — 탭 바 · 바닥 버튼
import { SnackbarAvoidOverlap, SnackbarProvider } from "@/components/ui/snackbar"
<SnackbarProvider>
<App />
<SnackbarAvoidOverlap>
<TabBar />
</SnackbarAvoidOverlap>
</SnackbarProvider>Behavior
| 인터랙션 | 동작 |
|---|---|
| 나타남 | 150ms 로 가운데에서 커지며 나타난다(모션 줄이기면 투명도만). 초점은 옮기지 않는다 |
| 시간 | 4초 · 액션이 있으면 6초 뒤 100ms 로 사라진다 |
| 마우스를 올림 · 손가락으로 누르고 있음 · 키보드 초점 | 멈춘다 — 떠나면 처음부터 다시 센다 |
| 새 띠가 옴 | 지금 띠를 바로 바꾼다 — 한 번에 하나. 지금 띠에 액션이 있으면 그 띠가 끝날 때까지 기다렸다 뜬다(기다리는 띠는 하나 — 마지막 것만) |
| 액션 누르기 | 액션을 하고 닫는다 |
Tab | 띠 안에서는 띠 → 액션 → 보조 기술용 닫기(초점이 오면 보인다). 웹은 페이지 마지막 칸 다음이 띠다. 앱은 화면의 Tab 순서가 띠에 닿지 않는다(띠가 시트 · 대화상자 위, 화면 밖에 그려진다) — 스크린 리더는 훑기로 닿고, 액션은 다른 길이 있다(위 "시간 · 한 번에 하나"). 모달이 열린 동안은 웹도 닿지 않는다 — 그 안에서 한 일의 결과는 그 안 Callout 이다(Q-003-2) |
| 화면 키보드가 올라옴 · 내려감 | 띠가 키보드 바로 위 8 로 옮겨 가고, 내려가면 원래 자리로 — 자리가 옮겨 갈 때는 200ms easing(모션 줄이기면 바로). 피할 자리(탭 바 · 바닥 버튼)와 둘 중 위쪽 |
Esc · 밀기 · 글 누르기 | 닫히지 않는다 — 닫기 버튼 · 시간 · 액션으로만 |
| 띠 안에 초점이 있을 때 닫힘 | 초점은 띠에 들어오기 전 자리로 돌아간다 |
| 시트 · 대화상자가 열린 동안 | 그 위에 그린다. 띠를 눌러도 그 창은 닫히지 않는다(바깥 누르기가 아니다). 창이 열린 채 봐야 할 결과는 그 안 Callout, 창을 닫으며 끝나는 일은 닫힌 뒤 스낵바 |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 글 fg-neutral-inverted 띠(bg-neutral-inverted) 위 16.41 · 다크 13.42, 액션 fg-brand-inverted Desk 6.90 · 7.76 · HR 6.88 · 7.56 ✓ |
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 아이콘 fg-positive-inverted 6.87 · 4.70, fg-critical-inverted 6.89 · 4.68, 띠 면 페이지 위 16.41 · 13.42 ✓. 포커스 링은 띠 글자색으로 띠 위에 그린다 ✓ |
| WCAG 2.2.1 Timing adjustable | 머무는 동안 멈추고 떠나면 처음부터. 액션은 6초 — 같은 일을 할 다른 길을 둔다 ✓ |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 액션 글 + 좌우 8 × 44 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 액션 높이 44 ✓ · 폭은 글에 따라 ⚠ |
| WCAG 4.1.3 Status messages | 자리는 aria-live="polite", 띠는 role="status" + aria-atomic="true" — 초점을 옮기지 않고 읽힌다. 조치가 필요한 오류는 스낵바가 아니라 자리에서(role="alert") |
| ARIA | 아이콘은 장식(aria-hidden) — 성공 · 실패는 글이 말한다. 사라지는 동안은 aria-hidden |
Do / Don't
✅ Do
- 방금 한 일의 결과를 한 문장으로 — "거래를 저장했어요."
- 되돌릴 수 있는 삭제에 "되돌리기" — 그리고 다른 길도 둔다. 되돌리기가 달린 띠는 다음 결과에 밀리지 않는다.
- 시트를 연 채 결과를 봐야 하면 그 안 Callout, 닫으며 끝나면 닫힌 뒤 스낵바.
- 탭 바 · 바닥 버튼은
SnackbarAvoidOverlap으로 피한다.
❌ Don't
- 모든 실패를 한곳에서 스낵바로 — 서버 글 그대로 · 중복.
- 조치가 필요한 오류 · 계속 보여야 할 경고를 스낵바에.
- 띠를 여러 장 쌓기.
- 시트 · 대화상자가 열린 채 그 안의 결과를 스낵바로 — 띠를 누르면 시트가 닫히고 액션이 먹지 않는다(그 안 Callout).
- "확인" 액션.
Specification
snackbar.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Snackbar 를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
region | enabled | 좌우 여백paddingX | spacing-x28px화면 끝과 8 — 안전 영역이 있으면 그만큼 더 |
| 아래 여백paddingBottom | spacing-x28px아래 8 + 안전 영역 — 탭 바 · 플로팅 버튼 · 바닥 버튼이 있으면 그 위 8(안전 영역과 둘 중 큰 쪽 — 탭 바는 안전 영역을 품고 있다). 화면 키보드가 올라와 있으면 키보드 바로 위 8 — 웹 폰 브라우저도 보이는 화면의 아래 끝을 잰다(2026-10-15 사용자 결정 — 구현 세션 질문 Q-003-1) | ||
| z-indexzIndex | 400z-snackbar — specs/z-index.md L6. 시트 · 대화상자 · 확인창 위 | ||
| keyboardGapkeyboardGap | spacing-x28px화면 키보드 위 끝 ↔ 띠 아래 끝 — 키보드도 피할 자리 하나다(안전 영역 · 탭 바 · 바닥 버튼과 넷 중 가장 위) | ||
| offsetDurationoffsetDuration | motion-duration-d4200ms피할 자리(탭 바 · 바닥 버튼 · 키보드)가 바뀌어 띠가 옮겨 갈 때 — 모션 줄이기면 바로(SEED snackbar.region.offsetDuration) | ||
| offsetEasingoffsetEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1)SEED snackbar.region.offsetTimingFunction | ||
root | enabled | 최소 높이minHeight | 44px |
| 너비width | 100%자리 폭 — 최대 464, 넓으면 가운데 | ||
| 최대 너비maxWidth | 464px | ||
| 여백padding | spacing-x2_510px | ||
| 모서리radius | radius-r28px | ||
| 배경background | bg-neutral-inverted#1A1F2E다크 #F5F6FA다크에서는 밝은 띠 | ||
| 그림자shadow | none그림자 없이 면 색으로 뜬다(SEED · Elevation) | ||
| 보이는 시간duration | 4000ms액션이 있으면 6000ms. 마우스 · 손가락 · 키보드 초점이 머무는 동안 멈추고, 떠나면 처음부터 다시 센다 | ||
icon | enabled | 아이콘size | 24pxlucide 선 아이콘 — 체크(circle-check) · 느낌표(circle-alert). 오른쪽 여백 2 를 이 칸 안에 둬 그림은 22 다(SEED 와 같다 — 글은 띠 가장자리에서 40) |
| 오른쪽 여백paddingRight | spacing-x0_52px칸 안쪽 — 칸을 넓히지 않는다 | ||
content | enabled | 좌우 여백paddingX | spacing-x1_56px글은 띠 가장자리에서 16(아이콘이 있으면 40) |
| 간격gap | spacing-x2_510px글과 액션은 양 끝 — 사이는 적어도 10 | ||
message | enabled | 글자·아이콘foreground | fg-neutral-inverted#FFFFFF다크 #242938 |
| 글자typography | text-t414px / 19px단어 단위 줄바꿈(v114) — 줄 수를 자르지 않는다(2026-09-22). 두 줄 안에서 끝나게 쓴다 | ||
| 굵기fontWeight | 400 | ||
action | enabled | 모서리radius | radius-r14px바탕이 없어 초점 링에만 보인다 |
| 글자·아이콘foreground | fg-brand-invertedDesk#7AA9F6다크 #0147ADHR#2ABE84다크 #075B3C반전 짝(v115) — 짙은 띠 위 Desk 6.90 · HR 6.88, 다크의 밝은 띠 위 7.76 · 7.56 | ||
| 글자typography | text-t414px / 19px | ||
| 굵기fontWeight | 700 | ||
| 터치 영역touchTarget | 글 + 좌우 8 × 44띠 높이 안에서 위아래로 넓힌다 | ||
closeButton | enabled | 크기size | 44px평소에는 보이지 않는다(보조 기술은 읽는다) — 키보드 초점이 오면 띠 오른쪽 끝에 X 16 으로 보인다 |
| 아이콘 크기iconSize | 16px | ||
| 바깥 여백margin | 위 · 아래 · 오른쪽 −10띠 여백(10) 안으로 들어와 띠 높이를 44 로 둔다 | ||
| 모서리radius | radius-r28px바탕이 없어 초점 링에만 보인다 | ||
| 색color | fg-neutral-inverted#FFFFFF다크 #242938 | ||
action | pressed | 배율scale | 2px 거리 축소글만 준다 — 모션 줄이기면 하지 않는다 |
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
focusRing | focused | 링 두께width | 2px |
| 링 색color | fg-neutral-inverted#FFFFFF다크 #242938띠 글자색 — 브랜드 링은 짙은 띠 위에서 Desk 1.96 이라 3:1 에 못 미친다 | ||
| 링 간격offset | -4px띠 · 닫기 — 안쪽 4. 링이 띠 글자색이라 바깥(페이지 위)에 그리면 보이지 않는다 | ||
| 액션 링 간격actionOffset | 2px액션 — 바깥 2. 띠 여백 안이라 띠 위에 그려진다 |
tone=positive
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-positive-inverted#25C062다크 #167F3F반전 짝(v115) — 짙은 띠 위 6.87, 다크의 밝은 띠 위 4.70 |
tone=critical
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
icon | enabled | 색color | fg-critical-inverted#FF8477다크 #D72323반전 짝(v115) — 짙은 띠 위 6.89, 다크의 밝은 띠 위 4.68 |
SEED 와 다른 점
- 아이콘 · 액션 색은 반전 짝 역할(v115 —
fg-positive-inverted·fg-critical-inverted·fg-brand-inverted) — SEED 는 밝은 바탕용 색을 그대로 써 짙은 띠 위에서 대비가 모자란다(다크 아이콘 2.5:1 · 액션 2.67:1). - 액션이 있으면 6초(사용자 결정 2026-10-02) — SEED 는 늘 4초.
- 글은 문장이면 마침표(Writing v106) — SEED 는 한 문장이면 뺀다.
- 모션 줄이기면 투명도만(v104) — SEED 는 그대로 돈다.
- 보조 기술용 닫기는 키보드 초점이 오면 보인다 — SEED 는 안 보이는 채로 Tab 이 선다.
- 포커스 링은 띠 글자색 — groo 브랜드 링은 짙은 띠 위에서 3:1 에 못 미친다.
- 아이콘은 lucide 선 아이콘(v106).
- z-index 는 specs/z-index.md 의 L6(
z-snackbar400). - 띠 안에서 닫히면 초점을 돌려준다 — 띠에 들어오기 전 자리로. SEED 는 body 로 떨어뜨려 키보드 사용자가 처음부터 다시 찾아야 한다.
- 화면 키보드가 있으면 키보드 바로 위 8(사용자 결정 2026-10-15) — SEED 는 키보드를 말하지 않는다.
- 아래 자리는 안전 영역과 피할 자리 중 큰 쪽 — SEED 는 둘을 더해, 안전 영역을 품은 탭 바 위에서는 그만큼 더 뜬다.
- 새 띠는 바로 바꾸고, 액션이 달린 띠만 지킨다 — SEED 디자인 문서는 늘 큐("현재 표시 중인 항목이 사라진 후 순차적으로"), SEED React 는 기본이 바로 바꿈(immediate)이고
strategy: "queued"를 띠마다 고른다. groo 는 액션(되돌리기 · 다시 시도)이 있는 띠가 떠 있을 때만 줄을 세운다(2026-10-22 사용자 결정 1B — https://claude.ai/artifact/NuEPiLRH2fJidXdKzcDsCX).
Migration notes
2026-10-22 — 액션 띠는 지킨다 · 알림 표는 한 벌 · 시트 안 결과 셋
사용자가 알리고 기다리는 것 정리 페이지에서 모두 추천 안으로 정했다.
- 새 띠(1B) — 바로 바꾸되, 지금 띠에 액션(되돌리기 · 다시 시도)이 있으면 끝날 때까지 지키고 새 띠는 뒤에(기다리는 띠는 하나 — 마지막 것만). 10-02 비교 페이지는 두 안 모두 "바로 바꾼다(SEED React 기본)" 로 그렸고 바꾸기 ↔ 줄 세우기는 묻지 않았다(따라오는 것). 레시피
snackbar.tsx의 상태(떠 있는 띠 하나 + 기다리는 것 하나)에 조건 하나를 더했다 · 사이트 데모 · 시간 그림 · yaml 모션 비고. 제품(웹 dssnackbar.tsx· 앱p_snackbar.dart)은 머지된 코드라 구현 지시로 고친다. - 시트 · 대화상자 안 결과(2B) — 결정(그 안 Callout · 스낵바는 닫힌 뒤)은 그대로, 상황 셋의 표로 풀어 썼다(열린 채 → Callout · 닫으며 → 닫힌 뒤 스낵바 · 칸 하나 → 칸 아래).
- "무엇으로 알리나" 표(3B) — 다섯 문서에 따로 있던 표를 이 문서 한 벌(상황 → 고를 것 + 다섯 부품 견주기)로 모으고, Callout · Page Banner · Help Bubble · Alert Dialog 는 자기 줄만 두고 링크한다. Page Banner ↔ Callout 표에 SEED 의 "용도" · "Sticky" 두 줄을 더했다. 값 변경 없음.
2026-10-15 — 키보드 위 자리 · 앱의 Tab
구현 세션 질문 두 가지를 정했다.
- 화면 키보드가 올라와 있으면 키보드 바로 위 8(사용자 결정 — Q-003-1). 앱은 이미 그렇다. 웹은
position: fixed; bottom만 써서 폰 브라우저에서 띠가 키보드 뒤로 갔다 — 보이는 화면(visualViewport)의 아래 끝을 재어 피할 자리에 더한다(레시피). 키보드가 있을 때 화면 위쪽에 띄우는 안 · 키보드가 내려간 뒤에 띄우는 안은 고르지 않았다. - 앱의 화면 Tab 은 띠에 닿지 않는다(정리 — Q-003-2). 띠는 화면 밖(시트 · 대화상자 위)에 그려진다. 액션은 다른 길이 있어야 하고("시간 · 한 번에 하나"), 시트 · 대화상자 안의 결과는 그 안 Callout 이라("자리") 키보드로 띠에 닿지 못해도 잃는 일이 없다. Behavior 의
Tab줄에 적었다. - 자리가 옮겨 갈 때의 200ms easing 을 YAML(
region.offsetDuration·offsetEasing)에 적었다 — 웹 · 앱이 이미 같은 값을 쓰고 SEEDsnackbar.region.offsetDuration과 같다.
2026-10-02 — SEED Snackbar 로 새로 둔다(옛 Sonner 를 대신)
사용자가 비교 페이지에서 정했다 — 알림을 일로 나누고(결과 · 가벼운 실패만 스낵바), 오류는 자리에서 알리고(전역 오류 토스트를 걷는다), SEED 모양 + 반전 짝 색, 아래 가운데, 액션이 있으면 6초, 마침표는 v106 그대로. 옛 Sonner(흰 카드 · 그림자 · 아이콘 넷 · 글 16/600 · 위 가운데 · 3장 쌓기)는 걷었다 — 옛 스펙은 sonner.history/v-pre-seed-feedback.*. 2026-09-22 의 "글은 자르지 않는다" 는 그대로 둔다.
제품은 앱 적용 단계에서 옮긴다(2026-10-02 조사 — Desk 웹 · HR 은 크로미움에 띄워 쟀고, Desk 앱은 위젯 테스트로 쟀다).
- Desk 웹 — sonner 37곳(위 가운데) + 손으로 만든 토스트 2(반복 거래 · 별빛, 아래 가운데 · 역할 없음). 전역 오류 토스트가 서버 글을 그대로(11곳은 화면이 또 띄운다). 시트가 열린 동안 띠를 누르면 시트가 닫히고 액션이 먹지 않는다. 키보드 초점에서 멈추지 않는다. 더치페이 "송금 요청을 보냈어요" 는 아무것도 보내지 않는다.
- Desk 앱 — showPSnackBar 36(아래). 전역 오류 스낵바(값을 바꾸는 요청만, 영어 대체 문구 "Server error"), 높이를 줄인 시트 4곳에서 띠가 시트 뒤에 그려져 안 보인다, 대화상자 위로는 딤 아래. 액션 스낵바가 스크린리더가 켜져도 6초 뒤 사라진다.
- HR 웹 — sonner 46(위 가운데, 앱 테마가 아니라 OS 테마, 스타일 클래스가 먹지 않는다). 전역 오류 토스트가 GET 까지 · 재시도마다(실패 한 번에 최대 4), 19곳은 화면이 또 띄운다. 저장 성공을 알리지 않는 화면이 많다(30파일).
Slider
정해진 범위에서 값을 끌어 고르는 컨트롤 — 예산 알림 임계값처럼 정확한 숫자보다 범위 안의 자리가 중요한 값, 그리고 별점처럼 2 ~ 5단계 가운데 하나를 고른다. 정확한 숫자 · 금액은 Input, 이름이 붙은 2 ~ 4개는 Chip, 5개 이상은 Select 다.
Swipe Actions
폰(768 미만) 목록 줄을 왼쪽으로 밀면 오른쪽에서 동작 버튼이 드러나는 지름길 — 줄을 열어 상세로 들어가지 않고도 수정 · 삭제 같은 잦은 동작에 바로 닿는다. iOS 메일 · 메시지의 스와이프와 같은 동작이다. 같은 동작은 늘 줄을 눌러 들어간 화면에도 있다 — 줄 끝 ⋮ 는 두지 않는다(Menu 의 "줄의 동작", 2026-10-25).