Dialog
1280 이상에서 화면 정중앙에 뜨는 대화상자. 입력 폼 · 상세처럼 지금 화면을 떠나지 않고 할 일을 띄운다 — 같은 내용을 1280 미만에서는 Bottom Sheet 로 띄운다(한 부품이 폭으로 바꾼다 — Responsive Dialog). 되돌릴 수 없는 확인은 Alert Dialog, 트리거에 붙는 짧은 내용은 Popover, 화면 높이를 넘는 긴 내용은 페이지다.
구조는 당근 SEED Dialog · Responsive Dialog(Apache-2.0)를 따른다 — 딤 · 대화상자 · 머리(제목 · 설명 · 닫기) · 본문 · 바닥, 본문만 스크롤. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-02 사용자 결정).
수치 원본은 dialog.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
크기 · 쓰임(입력 폼 · 조회) · 설명 · 본문 길이 · 창 폭(1280 위 · 아래)을 고르면 스펙대로 그린 대화상자(1280 미만이면 시트)와 그 코드가 바뀐다. 실제로 열고 닫고 본문을 스크롤할 수 있다.
창 폭은 그림 속 화면을 바꾼다(열린 채 바꾸면 쓰던 값은 그대로 표면만 바뀐다). large 는 이 그림 속 창보다 넓어 화면 폭 − 40 로 줄어든다.
import { Button } from "@/components/ui/button"
import { ResponsiveDialog, ResponsiveDialogBody, ResponsiveDialogCancel, ResponsiveDialogContent, ResponsiveDialogFooter } from "@/components/ui/dialog"
{/* form — 바깥 누르기 · 끌어내리기로 닫지 않는다. dirty — 닫기 전에 "작성한 내용이 사라져요" 를 묻는다 */}
<ResponsiveDialog open={open} onOpenChange={setOpen} form dirty={isDirty}>
<ResponsiveDialogContent title="휴가 신청" description="승인되면 알려드려요.">
<ResponsiveDialogBody>
<Field label="사유">…</Field>
<Field label="연락처">…</Field>
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
{/* 1280 이상에서만 그린다 — 시트에서는 위 닫기 버튼이 맡는다 */}
<ResponsiveDialogCancel>취소</ResponsiveDialogCancel>
<Button onClick={submit}>신청</Button>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Overlay | 딤 — 대화상자 뒤 화면 전체. 조회 대화상자는 누르면 닫힌다(입력 폼은 무시). |
| ⓑ Container | 대화상자 — 화면 정중앙, medium 480 · large 800, 높이는 화면의 80% 까지. |
| ⓒ Header | 머리 — 제목 · 설명. 조회 · 안내 대화상자만 오른쪽에 닫기 버튼. 두 단계 흐름의 2단계는 왼쪽에 ←(아래 "두 단계 흐름의 머리 ←"), 동작이 여럿인 상세는 닫기 왼쪽에 ⋮ 하나(아래 "머리 오른쪽 동작"). |
| ⓓ Body | 본문 — 넘치면 이 안에서만 스크롤한다(머리 · 바닥은 그대로). 넘칠 수 있는 본문은 끝을 흐린다(Scroll Fog). |
| ⓔ Footer | 바닥 — 버튼, 오른쪽 정렬. |
부위
| 부위 | 설명 |
|---|---|
overlay | 딤 — 대화상자 뒤 화면 전체 |
root | 대화상자 — 화면 정중앙 |
closeButton | 닫기 — 조회 · 안내 대화상자만. 입력 폼에는 두지 않는다(바닥 취소). 이름 "닫기" |
headerAction | 머리 오른쪽 동작 — 닫기 왼쪽(닫기가 없으면 오른쪽 끝)의 ⋮ 하나(축 headerAction). 이름 "{항목 이름} 더보기" |
header | 머리 — 제목 · 설명(· 닫기) |
title | 제목 |
description | 설명 — 덧붙일 말이 있을 때만 |
divider | 머리 아래 선 — 본문이 위로 스크롤됐을 때만 |
body | 본문 — 넘치면 이 안에서만 스크롤(머리 · 바닥은 고정) |
fog | 끝 흐림 — Scroll Fog(scroll-fog.yaml 의 overlayBody). 넘칠 수 있는 본문에만 걸고, 걸면 늘 켜져 있다 |
footer | 바닥 — 버튼, 오른쪽 정렬 |
focusRing | 키보드 포커스 링(웹) |
Properties
Size
medium 480 (기본) — 일반 입력 폼 · 상세, large 800 — 복잡한 설정 · 많은 조회. 높이는 내용만큼이고 화면 높이의 80% 를 넘지 않는다. 넘치는 만큼 본문이 스크롤된다.
- 기본급3,200,000원
- 식대200,000원
- 야근 수당184,000원
- 국민연금−144,000원
- 건강보험−113,440원
- 근무일22일
- 연장 근무8시간
- 쓴 연차1일
- 소득세−98,320원
- 실지급액3,228,240원
높이는 내용만큼, 화면 높이의 80% 까지 — 넘치는 만큼 본문이 스크롤된다. 화면이 좁으면 좌우 20 을 남기고 준다. 모서리 20 · 그림자 없음(딤과 표면 색으로 뜬다)
크기
| size | 너비 | 설명 |
|---|---|---|
medium (기본) | 480px | 480 — 기본. 일반 입력 폼 · 상세 |
large | 800px | 800 — 복잡한 설정 · 많은 조회 |
머리 · 본문 · 바닥
머리는 위 24 · 좌우 24 · 아래 16, 제목 22 / 30 · 700 · 설명 16 / 22 · 사이 6. 본문은 좌우 24. 바닥은 위 16 · 좌우 24 · 아래 24 에 버튼을 오른쪽으로 모은다 — Button small 36, [취소] [저장] 순서. 위로 스크롤하면 머리 아래 1px 선이 생긴다.
공통
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
overlay | 배경 | overlay-dim-light rgba(0, 0, 0, 0.50) · 다크 overlay-dim-dark rgba(0, 0, 0, 0.65) | groo 딤(v102) |
overlay | z-index | 100 | z-modal — specs/z-index.md L2 |
root | 최대 너비 | 화면 폭 − 40 | 좌우 20 은 남긴다 |
root | 최대 높이 | 화면 높이의 80% | dvh 우선 — 넘치면 본문만 스크롤 |
root | 모서리 | radius-r5 20px | |
root | 배경 | color-bg-layer-floating #FFFFFF · 다크 #2D3346 | |
root | 그림자 | none | 그림자 없이 딤과 표면 색으로 뜬다(SEED · Elevation) |
root | z-index | 101 | z-modal-content — specs/z-index.md L2 |
header | 위 여백 | spacing-x6 24px | |
header | 좌우 여백 | spacing-x6 24px | |
header | 아래 여백 | spacing-x4 16px | |
header | 간격 | spacing-x1_5 6px | 제목 ↔ 설명. 닫기 버튼이 있으면 오른쪽 52(24 + 아이콘 22 + 6) |
title | 글자 | text-t8 22px / 30px | |
title | 굵기 | 700 | |
title | 글자·아이콘 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
description | 글자 | text-t5 16px / 22px | |
description | 굵기 | 400 | |
description | 글자·아이콘 | color-fg-neutral-muted #535866 · 다크 #B7BDCC | |
closeButton | 크기 | 52px | 투명 상자 — 보이는 건 아이콘 22 |
closeButton | 모서리 | radius-r3 12px | |
closeButton | 아이콘 크기 | 22px | lucide x |
closeButton | 색 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | |
closeButton | 위 거리 | 28px | 아이콘이 대화상자 위에서 28 · 오른쪽 24 — 제목 첫 줄 가운데와 맞는다 |
closeButton | 오른쪽 거리 | spacing-x6 24px | |
closeButton | 터치 영역 | 52 × 52 | |
closeButton | 전환 대상 | background-color, scale | |
closeButton | 전환 시간 | motion-duration-color-transition 150ms | |
closeButton | 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | |
body | 좌우 여백 | spacing-x6 24px | |
body | 위 여백 | spacing-x6 24px | 머리가 없을 때만 — 머리가 있으면 0 |
body | 아래 여백 | spacing-x6 24px | 바닥이 없을 때만 — 바닥이 있으면 0 |
body | 세로 넘침 | auto | |
divider | 높이 | 1px | |
divider | 배경 | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D | 안쪽 그림자로 그린다 — 본문이 첫 자식이면(머리가 없으면) 그리지 않는다 |
divider | 전환 시간 | motion-duration-color-transition 150ms | |
divider | 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | |
footer | 위 여백 | spacing-x4 16px | |
footer | 좌우 여백 | spacing-x6 24px | |
footer | 아래 여백 | spacing-x6 24px | |
footer | 간격 | spacing-x2 8px | |
footer | 가로 맞춤 | flex-end | 오른쪽 정렬 — [취소] [저장] |
footer | 버튼 크기 | small 36 | Button small(Button 결정) |
본문 끝 흐림
목록 · 긴 폼처럼 넘칠 수 있는 본문은 Scroll Fog 를 건다(scrollFog) — 위 20 · 아래 80 이 늘 흐리고, 본문 안에 그만큼 여백을 둬 끝까지 내리면 흐림이 빈 여백 위에 놓인다. 넘쳤는지 재서 켜고 끄지 않는다 — 걸 본문인지는 내용의 종류로 정하고, 칸 두셋처럼 늘 들어맞는 본문에는 걸지 않는다. 1280 미만의 시트에서도 같다(Bottom Sheet).
본문 끝 흐림
| scrollFog | body 위 여백 | body 아래 여백 | body 스크롤 여유 | fog 크기 | fog 마스크 | 설명 |
|---|---|---|---|---|---|---|
off (기본) | — | — | — | — | — | 기본 — 늘 들어맞는 본문(칸 두셋 · 짧은 안내) |
on | 20px — 머리가 있으면 20(머리 아래 16 은 그대로 두고 본문 안에 더한다), 머리가 없으면 24 그대로 — 흐림 깊이 이상 | 80px — 바닥이 있어도 — 끝까지 내려도 마지막 줄이 흐림 아래 남지 않는다 | 위 20px · 아래 80px — 키보드로 옮긴 칸이 흐림 아래 멈추지 않게 | 위 20px · 아래 80px — 본문 상자의 위 · 아래 끝 | gradient-fade-mask — Scroll Fog overlayBody(scroll-fog.yaml) — 늘 켜져 있다. 넘쳤는지 재서 켜고 끄지 않는다 | 넘칠 수 있는 본문(목록 · 긴 폼 · 데이터에 따라 길어지는 내용) — 위 20 · 아래 80 이 늘 흐리다 |
State
| 상태 | 모습 |
|---|---|
enabled | 열림 — 딤 위 대화상자 |
scrolled | 본문이 위로 스크롤됨 — 머리 아래 1px stroke-neutral-subtle |
pressed | 닫기 버튼 — bg-layer-floating-pressed + 2px 거리 축소 |
focused | 키보드 포커스에만 링 2px · 띄움 2px |
상태
| 상태 | closeButton 배경 | closeButton 배율 | divider 배경 | divider 불투명도 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|---|---|
enabled 열림 | — | — | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D — 안쪽 그림자로 그린다 — 본문이 첫 자식이면(머리가 없으면) 그리지 않는다 | — | — | — | — |
scrolled 본문이 위로 스크롤됨 — 머리 아래 선 | — | — | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D — 안쪽 그림자로 그린다 — 본문이 첫 자식이면(머리가 없으면) 그리지 않는다 | 1 — 본문을 위로 스크롤하면 나타난다 | — | — | — |
pressed 닫기 버튼 누름 | color-bg-layer-floating-pressed #F7F8FD · 다크 #353B4D — SEED bg.transparent-pressed 의 떠 있는 표면 짝(v102) | 2px 거리 축소 — 기준 52 → 0.962 — 모션 줄이기면 하지 않는다 | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D — 안쪽 그림자로 그린다 — 본문이 첫 자식이면(머리가 없으면) 그리지 않는다 | — | — | — | — |
focused 웹 — 키보드 포커스에만 링 | — | — | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D — 안쪽 그림자로 그린다 — 본문이 첫 자식이면(머리가 없으면) 그리지 않는다 | — | 2px | 2px | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
열림 — 대화상자 | motion-duration-d4 200ms | motion-ease-enter-expressive cubic-bezier(0.03, 0.4, 0.1, 1) | scale 1.3 → 1, opacity 0 → 1 | 크게 나타나며 줄어든다(SEED) — 모션 줄이기면 150ms 서서히 나타남 |
열림 — 딤 | motion-duration-d2 100ms | motion-ease-enter cubic-bezier(0, 0, 0.15, 1) | opacity 0 → 1 | |
닫힘 — 대화상자 | motion-duration-d2 100ms | motion-ease-exit cubic-bezier(0.35, 0, 1, 1) | opacity → 0 | 축소 없이 사라진다 |
닫힘 — 딤 | motion-duration-d2 100ms | motion-ease-exit cubic-bezier(0.35, 0, 1, 1) | opacity → 0 | |
머리 아래 선 | motion-duration-color-transition 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | box-shadow | |
누름 — 닫기 버튼 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale |
Guidelines
한 부품이 폭으로 바뀐다
폼 · 상세는 한 부품(ResponsiveDialog)으로 짠다 — 1280 이상에서 Dialog, 미만에서 Bottom Sheet 다. 머리 · 본문 · 바닥은 같고, 닫는 자리만 표면에 맞춰 바뀐다(아래 "닫기 버튼과 취소"). 경계는 Input Button 과 같은 1280 이라, 대화상자 안의 날짜 칸은 늘 팝오버로, 시트 안의 날짜 칸은 늘 시트로 열린다 — 표면이 엇갈려 겹치지 않는다.
입력 폼은 실수로 닫히지 않게
입력 폼 대화상자는 바깥(딤)을 눌러도 닫히지 않는다. 바닥 [취소] · Esc 로 닫고(1280 미만의 시트는 뒤로 가기로도), 바뀐 값이 있으면 닫기 전에 "작성한 내용이 사라져요" 를 묻는다(Field 의 규칙). 조회 · 안내 대화상자는 바깥 누르기 · Esc 로 닫힌다.
닫기 버튼과 취소 — 하나만
| 대화상자 | 닫는 자리 |
|---|---|
| 입력 폼(저장 · 신청처럼 주 작업이 있다) | 바닥 [취소] [저장] — 머리 닫기 버튼 없음 |
| 조회 · 안내 | 머리 닫기 버튼 — 바닥 버튼은 다른 동작(수정 · 삭제)이 있을 때만 |
닫기 버튼과 바닥 취소를 함께 두지 않는다. 1280 미만의 시트에서는 입력 폼도 위 닫기 버튼 + 바닥 [저장] 이다(Bottom Sheet).
두 단계 흐름의 머리 ←
한 부품 안에서 1단계(종류 고르기) → 2단계(폼)로 가는 흐름은 1280 이상의 대화상자에서도 2단계 머리 왼쪽에 ← 를 단다 — 시트와 같은 부품(onBack) · 같은 값(보이는 원 40 · 아이콘 24 · 누르는 영역 44 · 아이콘이 왼쪽 24 · 제목은 원 오른쪽 4 에서 · 제목 첫 줄 가운데)이다. 수치 원본은 bottom-sheet.yaml 의 축 back 하나다. ← 는 1단계로 한 단계 되돌리고, 쓴 값이 있으면 "작성한 내용이 사라져요" 를 묻고 돌아가며 그 값을 버린다. 입력 폼 대화상자라 머리 닫기 버튼은 없고 바닥 [취소] 가 닫는다 — ← 는 닫지 않는다. 규칙 전문은 Bottom Sheet 의 "두 단계 시트의 머리 ←"(2026-10-29 사용자 결정 17-1 Q3).
머리 오른쪽 동작 — ⋮ 하나
들어간 상세에서 그 항목의 동작이 여럿이면 머리 오른쪽에 ⋮ 하나를 둔다(headerAction) — 시트와 같은 부품 · 같은 규칙이다(Bottom Sheet 의 "머리 오른쪽 동작"). 누르면 Menu 가 열린다. 자리는 닫기 버튼 왼쪽 사이 4 — 닫기가 없는 입력 폼은 오른쪽 끝이다. 단추는 Button ghost 아이콘만(보이는 36 · 누르는 44), 이름 "{항목 이름} 더보기", 세로는 제목 첫 줄 가운데(닫기 아이콘과 같다). 제목 · 설명은 단추 자리를 비운 남은 폭에서 줄을 바꾼다. 초점 차례는 제목 → 동작 → 닫기, 머리 동작은 하나만 둔다(2026-10-25 I-20 — 웹 DialogContent.headerAction).
머리 오른쪽 동작
| headerAction | headerAction 크기 | headerAction 간격 | headerAction 터치 영역 | headerAction 이름 | headerAction 버튼 | headerAction centerY | headerAction focusOrder | header 오른쪽 여백 | 설명 |
|---|---|---|---|---|---|---|---|---|---|
hidden (기본) | — | — | — | — | — | — | — | — | 기본 |
shown | 36px | spacing-x1 4px — 닫기 아이콘 왼쪽 끝 ↔ 동작 단추 4. 닫기가 없는 입력 폼은 오른쪽 끝 — 단추 상자가 대화상자 오른쪽에서 16 | 44 × 44 | {항목 이름} 더보기 — 아이콘만 있는 단추 — 항목 이름을 앞에 둔다 | Button ghost iconOnly small — 보이는 36 · 아이콘 16 — 누르는 영역 44 × 44 | 제목 첫 줄 가운데 — 대화상자 위 24 + 줄 높이 30 의 절반 = 39 — 닫기 아이콘 가운데와 같다 | 제목 → 동작 → 닫기 — DOM 차례 그대로 — ← 가 있으면 ← → 제목 → 동작 → 닫기 | 100px — 닫기 자리 52 + 사이 4 + 단추 36 + 8 — 제목 · 설명은 남은 폭에서 줄을 바꾼다. 닫기가 없는 입력 폼은 60(오른쪽 16 + 단추 36 + 8) | 들어간 상세에서 동작이 여럿일 때 — 머리 ⋮ 하나가 Menu 를 연다. 머리 동작은 하나만 둔다 |
Alert Dialog 와 견주기
SEED Dialog 문서의 비교 표 네 줄(쓰는 일 · 사용자 응답 · 닫기 버튼 · 크기)에 groo 에서 갈리는 세 줄을 더했다(2026-10-22 사용자 결정 1C — https://claude.ai/artifact/YRPbWzhgD4EDdKsVioU8Zj). 어느 떠 있는 표면을 쓸지 일곱을 한눈에 견준 표는 DESIGN.md Components 의 "시트 · 대화상자 · 확인창 · 팝오버" 한 벌이다(3C).
| Dialog | Alert Dialog | |
|---|---|---|
| 쓰는 일 | 작업 · 조회 · 입력 폼 — 사용자가 내용과 오가며 일한다 | 되돌릴 수 없는 일의 확인 · 꼭 알아야 할 알림 |
| 사용자 응답 | 고르지 않고 닫고 돌아가도 된다 | 고르기 전에는 못 지나간다 |
| 닫기 버튼 | 조회는 머리 닫기 · 입력 폼은 바닥 [취소](둘을 함께 두지 않는다) | 없다 — [취소] 가 닫는다 |
| 크기 | medium · large, 높이는 화면의 80% 까지(본문만 스크롤) | 하나(최대 272), 넘치면 확인창 전체가 스크롤 |
바깥(딤) · Esc (groo) | 조회는 둘 다 닫힘 · 입력 폼은 바깥 무시, Esc 는 취소 | 바깥 무시 · Esc 와 뒤로 가기는 취소 |
| 1280 미만 (groo) | 같은 내용이 Bottom Sheet 로 바뀐다(Responsive Dialog) | 폰에서도 가운데 그대로 |
| 버튼 (groo) | 바닥 오른쪽 small — 시트에서는 large | 반씩 나란히(길면 세로 · 확정 위), 1280 미만 medium · 이상 small |
대화상자에 두지 않는 것
- 되돌릴 수 없는 확인 → Alert Dialog.
- 트리거에 붙는 짧은 내용(안내 · 고르기 패널) → Popover.
- 화면 높이를 넘는 긴 내용 · 단계가 많은 일 → 페이지.
- 대화상자 위에 대화상자 — 겹쳐도 되는 건 그 안에서 연 Alert Dialog · Popover 뿐이다.
글
제목은 하는 일 — "휴가 신청" · "거래 상세". 설명은 덧붙일 말이 있을 때만 한 문장(해요체 · 마침표). 바닥 버튼은 동작 이름("신청" · "저장") — "확인" 으로 뭉뚱그리지 않는다.
코드
레시피 recipes/shadcn/components/ui/dialog.tsx 를 쓴다(Radix Dialog 위). 폼 · 상세는 ResponsiveDialog — 1280 에서 Dialog 와 Bottom Sheet 를 바꾸고, form 이면 닫는 자리도 표면에 맞춰 바꾼다(대화상자는 바닥 취소, 시트는 위 닫기). form 이 아니면(조회 · 안내) 두 표면 모두 위 닫기 버튼이다. 본문에 List 를 바로 두면 줄이 제 좌우 여백(24)을 가지므로 본문 좌우 여백을 뺀다(className="px-0"). 넘칠 수 있는 본문은 scrollFog 를 준다(위 20 · 아래 80 끝 흐림 — Scroll Fog). 아래 미리보기는 스펙 값으로 그린 모습이다.
입력 폼
남은 연차 11일
import { Button } from "@/components/ui/button"
import { ResponsiveDialog, ResponsiveDialogBody, ResponsiveDialogCancel, ResponsiveDialogContent, ResponsiveDialogFooter } from "@/components/ui/dialog"
{/* form — 바깥 누르기 · 끌어내리기로 닫지 않는다. dirty — 닫기 전에 "작성한 내용이 사라져요" 를 묻는다 */}
<ResponsiveDialog open={open} onOpenChange={setOpen} form dirty={isDirty}>
<ResponsiveDialogContent title="휴가 신청" description="승인되면 알려드려요.">
<ResponsiveDialogBody>
<Field label="휴가 종류">…</Field>
<Field label="기간">…</Field>
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
{/* 1280 이상에서만 그린다 — 시트에서는 위 닫기 버튼이 맡는다 */}
<ResponsiveDialogCancel>취소</ResponsiveDialogCancel>
<Button onClick={submit}>신청</Button>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>조회
- 지하철교통 · 교통 체크카드-1,450원
{/* 조회 — 머리 닫기 버튼(시트에서는 오른쪽 위 원), 바깥 누르기로도 닫힌다. 길이가 데이터에 따라 느는 본문이라 scrollFog */}
<ResponsiveDialog open={open} onOpenChange={setOpen}>
<ResponsiveDialogContent title="거래 상세">
<ResponsiveDialogBody scrollFog className="px-0">
<List>…</List>
</ResponsiveDialogBody>
</ResponsiveDialogContent>
</ResponsiveDialog>Behavior
| 인터랙션 | 동작 |
|---|---|
| 열기 | 200ms 로 크게 나타나며 줄어든다(1.3 → 1). 처음 초점은 대화상자에 간다. |
바닥 취소 · 닫기 버튼 · Esc | 닫는다. 입력 폼에 바뀐 값이 있으면 먼저 묻는다. 뒤로 가기로 닫는 것은 1280 미만(시트)뿐이다 — 데스크톱 브라우저의 뒤로 가기는 페이지를 떠난다. |
| 바깥(딤) 누르기 | 조회 · 안내는 닫는다. 입력 폼은 무시한다. |
| 머리 ⋮ 누르기 | 그 항목의 동작 목록 Menu 를 연다(대화상자 위 L3). |
| 머리 ← 누르기 | 두 단계 흐름의 2단계 — 같은 대화상자 안에서 1단계로 한 단계 되돌린다(닫지 않는다). 쓴 값이 있으면 먼저 "작성한 내용이 사라져요" 를 묻고, 돌아가면 그 값을 버린다. |
| 본문 스크롤 | 본문만 스크롤 — 위로 스크롤되면 머리 아래 선. 끝 흐림(scrollFog)은 걸었으면 늘 그대로다. |
| 닫힌 뒤 | 100ms 로 사라지고, 초점은 연 자리(트리거)로 돌아간다. |
| 열린 동안 | 뒤 화면을 보조 기술에서 숨기고 스크롤을 잠근다. 초점은 대화상자 안을 돈다. |
| 창 폭이 1280 을 넘나듦 | 열린 채 표면이 바뀐다(대화상자 ↔ 시트). 값은 폼(부모)이 들고 있어 그대로다 — 입력칸의 값을 대화상자 · 시트 안에만 두지 않는다. |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 제목 fg-neutral 떠 있는 표면 위 16.41 · 다크 11.62, 설명 fg-neutral-muted 7.11 · 6.67 ✓ |
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 닫기 아이콘 fg-neutral-subtle 5.50 · 5.27 ✓. 키보드 포커스 링 Desk 8.38 · 5.28 · HR 8.17 · 5.26 ✓ |
| WCAG 2.4.3 Focus order | 열면 대화상자로, 닫으면 연 자리로 초점이 간다. 열린 동안 초점이 뒤 화면으로 나가지 않는다 |
| WCAG 1.4.10 Reflow | 1280 미만은 시트로 바뀌고, 대화상자 높이는 화면의 80% 까지 — 넘치면 본문이 스크롤된다(머리 · 바닥이 화면 밖으로 나가지 않는다) |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 닫기 버튼 52 · 머리 ⋮ 36 · 바닥 버튼 36 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 닫기 버튼 52 ✓ · 머리 ⋮ · 바닥 버튼은 누르는 영역을 44 까지 넓힌다(Button) ✓ |
| ARIA | role="dialog" + aria-modal="true", 제목 aria-labelledby · 설명 aria-describedby(있을 때만), 닫기 버튼 이름 "닫기", 머리 ← 이름 "뒤로"(초점 차례 맨 앞), 머리 ⋮ 이름 "{항목 이름} 더보기"(제목 → 동작 → 닫기) |
Do / Don't
✅ Do
- 폼 · 상세는 한 부품으로 — 1280 이상 대화상자, 미만 시트.
- 입력 폼은 바닥 [취소] [저장], 조회는 머리 닫기 버튼.
- 입력 폼은 바깥 누르기로 닫지 않고, 바뀐 값이 있으면 닫기 전에 묻는다.
- 긴 내용은 본문만 스크롤 — 머리 · 바닥은 늘 보인다.
- 두 단계 흐름의 2단계는 머리 왼쪽 ← 로 1단계에 돌아간다(시트와 같은 값).
❌ Don't
- 머리 닫기 버튼과 바닥 취소를 함께.
- 되돌릴 수 없는 확인을 Dialog 로(Alert Dialog).
- 높이 상한 없이 내용을 늘려 제목 · 버튼이 화면 밖으로.
- 대화상자 위에 대화상자 — 단계가 많으면 페이지 · 단계로.
Specification
dialog.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Dialog 를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다. 1280 미만의 모습은 Bottom Sheet 의 Specification 이다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
overlay | enabled | 배경background | overlay-dim-lightrgba(0, 0, 0, 0.50)다크 overlay-dim-darkrgba(0, 0, 0, 0.65)groo 딤(v102) |
| z-indexzIndex | 100z-modal — specs/z-index.md L2 | ||
root | enabled | 최대 높이maxHeight | 화면 높이의 80%dvh 우선 — 넘치면 본문만 스크롤 |
| 최대 너비maxWidth | 화면 폭 − 40좌우 20 은 남긴다 | ||
| 모서리radius | radius-r520px | ||
| 배경background | bg-layer-floating#FFFFFF다크 #2D3346 | ||
| 그림자shadow | none그림자 없이 딤과 표면 색으로 뜬다(SEED · Elevation) | ||
| z-indexzIndex | 101z-modal-content — specs/z-index.md L2 | ||
closeButton | enabled | 크기size | 52px투명 상자 — 보이는 건 아이콘 22 |
| 아이콘 크기iconSize | 22pxlucide x | ||
| 모서리radius | radius-r312px | ||
| 색color | fg-neutral-subtle#62697A다크 #A2A8B7 | ||
| 터치 영역touchTarget | 52 × 52 | ||
| 전환 대상transitionProperty | background-color, scale | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
| 위 거리top | 28px아이콘이 대화상자 위에서 28 · 오른쪽 24 — 제목 첫 줄 가운데와 맞는다 | ||
| 오른쪽 거리right | spacing-x624px | ||
header | enabled | 좌우 여백paddingX | spacing-x624px |
| 위 여백paddingTop | spacing-x624px | ||
| 아래 여백paddingBottom | spacing-x416px | ||
| 간격gap | spacing-x1_56px제목 ↔ 설명. 닫기 버튼이 있으면 오른쪽 52(24 + 아이콘 22 + 6) | ||
title | enabled | 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA |
| 글자typography | text-t822px / 30px | ||
| 굵기fontWeight | 700 | ||
description | enabled | 글자·아이콘foreground | fg-neutral-muted#535866다크 #B7BDCC |
| 글자typography | text-t516px / 22px | ||
| 굵기fontWeight | 400 | ||
divider | enabled | 높이height | 1px |
| 배경background | stroke-neutral-subtle#EDEFF3다크 #353B4D안쪽 그림자로 그린다 — 본문이 첫 자식이면(머리가 없으면) 그리지 않는다 | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
body | enabled | 좌우 여백paddingX | spacing-x624px |
| 위 여백paddingTop | spacing-x624px머리가 없을 때만 — 머리가 있으면 0 | ||
| 아래 여백paddingBottom | spacing-x624px바닥이 없을 때만 — 바닥이 있으면 0 | ||
| 세로 넘침overflowY | auto | ||
footer | enabled | 버튼 크기buttonSize | small 36Button small(Button 결정) |
| 좌우 여백paddingX | spacing-x624px | ||
| 위 여백paddingTop | spacing-x416px | ||
| 아래 여백paddingBottom | spacing-x624px | ||
| 간격gap | spacing-x28px | ||
| 가로 맞춤justifyContent | flex-end오른쪽 정렬 — [취소] [저장] | ||
divider | scrolled | 불투명도opacity | 1본문을 위로 스크롤하면 나타난다 |
closeButton | pressed | 배경background | bg-layer-floating-pressed#F7F8FD다크 #353B4DSEED bg.transparent-pressed 의 떠 있는 표면 짝(v102) |
| 배율scale | 2px 거리 축소기준 52 → 0.962 — 모션 줄이기면 하지 않는다 | ||
focusRing | focused | 링 두께width | 2px |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | 2px |
size=medium
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 너비width | 480px |
size=large
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 너비width | 800px |
headerAction=shown
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
headerAction | enabled | 크기size | 36px |
| 간격gap | spacing-x14px닫기 아이콘 왼쪽 끝 ↔ 동작 단추 4. 닫기가 없는 입력 폼은 오른쪽 끝 — 단추 상자가 대화상자 오른쪽에서 16 | ||
| 터치 영역touchTarget | 44 × 44 | ||
| 이름name | {항목 이름} 더보기아이콘만 있는 단추 — 항목 이름을 앞에 둔다 | ||
| 버튼button | Button ghost iconOnly small보이는 36 · 아이콘 16 — 누르는 영역 44 × 44 | ||
| centerYcenterY | 제목 첫 줄 가운데대화상자 위 24 + 줄 높이 30 의 절반 = 39 — 닫기 아이콘 가운데와 같다 | ||
| focusOrderfocusOrder | 제목 → 동작 → 닫기DOM 차례 그대로 — ← 가 있으면 ← → 제목 → 동작 → 닫기 | ||
header | enabled | 오른쪽 여백paddingRight | 100px닫기 자리 52 + 사이 4 + 단추 36 + 8 — 제목 · 설명은 남은 폭에서 줄을 바꾼다. 닫기가 없는 입력 폼은 60(오른쪽 16 + 단추 36 + 8) |
scrollFog=on
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
body | enabled | 위 여백paddingTop | 20px머리가 있으면 20(머리 아래 16 은 그대로 두고 본문 안에 더한다), 머리가 없으면 24 그대로 — 흐림 깊이 이상 |
| 아래 여백paddingBottom | 80px바닥이 있어도 — 끝까지 내려도 마지막 줄이 흐림 아래 남지 않는다 | ||
| 스크롤 여유scrollPadding | 위 20px · 아래 80px키보드로 옮긴 칸이 흐림 아래 멈추지 않게 | ||
fog | enabled | 크기size | 위 20px · 아래 80px본문 상자의 위 · 아래 끝 |
| 마스크mask | gradient-fade-masklinear-gradient(#00000000 0%, #00000003 8%, #00000005 16%, #0000000d 22%, #00000014 29%, #00000021 35%, #0000002e 41%, #00000040 47%, #00000052 53%, #00000066 59%, #0000007a 65%, #00000094 71%, #000000ab 78%, #000000c7 84%, #000000e3 92%, #000000ff 100%)Scroll Fog overlayBody(scroll-fog.yaml) — 늘 켜져 있다. 넘쳤는지 재서 켜고 끄지 않는다 |
SEED 와 다른 점
- 바뀌는 폭은 1280(사용자 결정 2026-10-02) — SEED Responsive Dialog 는 md(768). groo 는 Input Button 의 시트 · 팝오버 경계(1280)와 하나로 맞췄다.
- 바닥 버튼은 Button small 36 — SEED 예제는 medium 40. Button 을 정할 때 "40 은 대화상자에 비해 굵다" 로 고른 값을 지켰다(2026-10-02 다시 확인).
- 딤은 groo 0.50 · 다크 0.65(v102) — SEED 0.455.
- 입력 폼은 바깥을 눌러도 닫히지 않고, 머리 닫기 버튼을 두지 않는다 — SEED 도 같은 쪽을 권한다(snippet 의 바깥 닫기 기본값 · "입력 폼에서는 헤더 X 지양"). groo 는 규칙으로 못박았다.
- 반투명 색을 불투명 짝으로(v102) — 머리 아래 선
stroke-neutral-subtle, 닫기 버튼 누름bg-layer-floating-pressed. - 본문 끝 흐림은 Scroll Fog(2026-10-03) — 넘칠 수 있는 본문에 늘 켜진 위 20 · 아래 80. SEED Dialog 는 본문이 넘칠 때만 아래 48 을 흐린다.
- z-index 는 specs/z-index.md 의 L2(딤
z-modal100 · 대화상자z-modal-content101) — 그 안에서 연 Popover(L3) · Alert Dialog(L5)가 위에 뜬다(SEED 는 모두 2 + layerIndex).
Migration notes
2026-10-30 — 머리 오른쪽 동작
구현 검수(I-20)에서 정해 웹 ds 가 먼저 만든 머리 ⋮ 자리를 적었다 — 시트와 같은 규칙이고, 자리 값만 대화상자의 닫기(52 상자 · 아이콘 22)에 맞췄다. dialog.yaml 에 축 headerAction · 부위 headerAction(Button ghost 아이콘만 36 · 닫기 아이콘 왼쪽 4 · 닫기가 없으면 오른쪽 16 · 제목 첫 줄 가운데 · 머리 오른쪽 100 / 60)을 더했다 — 웹 DialogContent.headerAction(67de7f1c)의 값이다. 레시피 DialogContent · ResponsiveDialogContent 에 headerAction 을 더했다. 새 토큰 없음. 옛 스펙은 dialog.history/v-pre-cleanup-1030.*.
2026-10-29 — 두 단계 흐름의 머리 ←
사용자 결정 17-1 Q3 = A — 한 부품 안 두 단계 흐름(자산 추가 — 종류 고르기 → 폼)의 2단계 머리 왼쪽 ← 를 1280 이상의 대화상자에도 같은 값으로 둔다. 고르지 않은 안 B(2단계 폼 맨 위 "종류" 칸)와 결정 전문은 Bottom Sheet 의 같은 날 Migration notes. 수치는 bottom-sheet.yaml 의 축 back 한 벌이고 dialog.yaml 은 고치지 않았다. 레시피 DialogContent · ResponsiveDialogContent 에 onBack 을 더했다.
2026-10-22 — 대화상자 위 대화상자의 갈 곳
Don't "대화상자 위에 대화상자" 는 그대로 두고 옛 문장의 대안 "단계가 많으면 페이지 · 단계로" 를 함께 적었다(남은 규칙 R18 — 그대로, https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L). 값 변경 없음.
2026-10-22 — Alert Dialog 와 견주는 표
SEED Dialog 문서의 "Dialog vs Alert Dialog" 네 줄에 groo 의 세 줄(바깥 · Esc · 1280 미만 모양 · 버튼)을 더한 표를 두었다 — 폰에서 Dialog 는 시트가 되고 확인창만 가운데에 남는 것이 SEED 에 없는 차이다(사용자 결정 1C — https://claude.ai/artifact/YRPbWzhgD4EDdKsVioU8Zj). 떠 있는 표면 일곱을 고르는 큰 표는 DESIGN.md 한 벌로(3C). 값 변경 없음.
2026-10-03 — 본문 끝 흐림을 Scroll Fog 로
사용자가 비교 페이지에서 끝 흐림을 SEED 의 늘 켜진 마스크(Scroll Fog)로 정해, 2026-10-02 의 "본문이 넘칠 때만 아래 48 흐림" 을 바꿨다 — 넘칠 수 있는 본문(목록 · 긴 폼)에 scrollFog 를 걸고, 걸면 위 20 · 아래 80 이 늘 흐리다(본문 안 여백도 그만큼). 넘친 쪽만 흐리는 안은 고르지 않았다. 상태 overflow 와 부위 fade 를 걷고 축 scrollFog · 부위 fog 를 두었다.
2026-10-02 — SEED Dialog 로 다시 정한다
사용자가 비교 페이지에서 정했다 — 일로 나누기(폼 · 상세는 한 부품이 폭으로 시트 ↔ 대화상자) · 경계 1280 · 입력 폼은 바깥으로 닫지 않음 · 닫기 버튼과 취소는 하나만 · SEED 모양(medium 480 · large 800 · 최대 80% · 모서리 20 · 제목 22 / 30 · 본문만 스크롤 + 흐림 · 머리 아래 선). 바닥 버튼은 Button 결정대로 small 36. 옛 Dialog(sm 420 · md 520 · lg 720 · 86vh · 모서리 12 · 제목 18 / 600 · 머리 X + 바닥 취소)는 걷었다 — 옛 스펙은 dialog.history/v-pre-seed-overlay.*.
제품은 앱 적용 단계에서 옮긴다(2026-10-02 조사).
- Desk 웹 — ModalShell 53곳(768 에서 시트 ↔ 대화상자 → 1280). 열어도 초점이 안으로 들어가지 않고 Tab 이 가려진 뒤 화면을 돌며, 닫으면 초점이 body 로 떨어진다. ModalShell 의
aria-describedby가 없는 id 를 가리킨다. 폼 33곳이 바깥 누르기로 경고 없이 닫힌다. 상세 바닥의 삭제(dangerSoft)는 4.01:1. - Desk 앱 — 시트가 대부분이고 프리셋 저장만 가운데 대화상자(PFormAlertDialog)다(→ 시트). 폼 29곳이 경고 없이 닫힌다.
- HR 웹 — Dialog 30곳 중 18곳이 높이 상한이 없어 긴 내용이면 제목과 버튼이 화면 밖으로 나간다(데스크톱 y −161 · 모바일 y −671). 트리거 없이 연 21곳은 닫으면 초점이 body 로 떨어진다. 닫기 버튼 16×16 "Close", 딤 0.80, 설명 4.42:1.
- 앱 적용 때 화면마다 정할 자리 — HR 전체 화면 Dialog 3(페이지 · 시트 · Side Panel), 데스크톱 필터(Dialog · Popover · Side Panel), 반복 거래 상세의 바닥 액션 3개.
Date Picker
날짜 · 기간 · 여러 날을 고르는 달력. 날짜 칸(Input Button)을 누르면 1280 미만은 아래 시트 · 이상은 칸 아래 팝오버로 열리고, "완료" 로 넣는다 — 날짜 고르기가 그 화면의 주된 일이면 화면에 바로 펼친다(지금 쓰는 곳은 없다 — "어디에 놓나"). 날짜를 치는 칸은 두지 않는다. 시각은 Time Picker 를 날짜 칸 옆에 나란히, 달만 고를 때는 Wheel Picker 다.
Divider
내용 사이를 나누는 1px 선. 같은 묶음 안은 들인 선, 묶음 사이는 끝까지 선이고, 크게 다른 내용 사이는 선이 아니라 8 간격(회색 바탕 위 흰 층 사이)이다. 반복되는 목록 줄 사이는 List 의 줄 사이 선(필요할 때만)이다.