Popover
트리거에 붙어 떠 있는 표면. 1280 이상에서 부가 정보 · 짧은 동작 · 고르는 패널(Input Button 의 달력 · 시각 · 목록)을 띄운다 — 같은 내용을 1280 미만에서는 Bottom Sheet 로 띄운다. 비모달이라 뒤 화면을 막지 않는다. 폼 · 상세는 Dialog, 줄의 동작 목록은 Menu(그 차례에), 짧은 도움말 말풍선은 Help Bubble(그 차례에)이다.
구조는 당근 SEED Popover(Apache-2.0 — 디자인 문서는 없고 React 문서 · rootage 만 있다)를 따른다 — 표면 · 머리(제목 · 설명 · 닫기) · 본문 · 바닥, 머리 · 본문 · 바닥 여백은 Dialog 와 같다. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-02 사용자 결정).
수치 원본은 popover.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
머리 · 바닥 · 본문 길이 · 트리거 자리(화면 위 · 아래 · 가장자리)를 고르면 스펙대로 그린 팝오버와 그 코드가 바뀐다. 실제로 열고 Tab 으로 빠져나가 닫을 수 있다.
열면 초점이 팝오버 안으로 간다(가두지 않는다) — 마지막에서 Tab 을 누르면 다음 자리로 나가며 닫힌다. Esc 는 닫고 트리거로, 바깥을 누르면 누른 자리로(초점을 못 받는 자리면 트리거로) 간다. 1280 미만에서는 같은 내용을 시트로 띄운다(이 그림은 1280 이상의 창이다).
import { Info } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Popover, PopoverBody, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="xsmall" layout="iconOnly" aria-label="연차 사용 규정"><Info /></Button>
</PopoverTrigger>
<PopoverContent title="연차 사용 규정">
<PopoverBody>입사 1년 미만은 한 달에 1일씩 생기고, 1년이 지나면 15일이 생겨요.</PopoverBody>
</PopoverContent>
</Popover>Anatomy
폭 320 ~ 480 · 높이 600 까지 · 모서리 20 · 그림자 s3 · 트리거와 8 · 화면 가장자리와 16 — 머리 위 24 · 좌우 24 · 아래 16, 제목 20 / 27 · 설명 14 / 19 · 닫기 아이콘 22(누르는 영역 52) · 바닥 위 16 · 아래 24 · 머리 · 바닥이 없으면 본문 위 · 아래 24
| 부위 | 설명 |
|---|---|
| ⓐ Container | 표면 — 트리거 아래(모자라면 위), 폭 320 ~ 480, 그림자 s3. |
| ⓑ Header | 머리 — 제목 · 설명 · 닫기. 없어도 된다(고르는 패널). |
| ⓒ Body | 본문 — 넘치면 이 안에서 스크롤. 넘칠 수 있는 본문은 끝을 흐린다(Scroll Fog — Dialog 와 같다). |
| ⓓ Footer | 바닥 — 버튼, 오른쪽 정렬. 없어도 된다. |
부위
| 부위 | 설명 |
|---|---|
root | 표면 — 트리거 아래(모자라면 위) |
closeButton | 닫기 — 머리가 있을 때. 이름 "닫기" |
header | 머리 — 제목 · 설명 · 닫기(없어도 된다) |
title | 제목 |
description | 설명 |
body | 본문 — 넘치면 이 안에서 스크롤 |
footer | 바닥 — 버튼(없어도 된다) |
focusRing | 키보드 포커스 링(웹) |
notificationList | 알림 목록 — Desk 데스크톱 머리 벨이 여는 팝오버(1280 이상). 같은 내용을 1280 미만은 Bottom Sheet |
Properties
크기 · 자리
폭은 320 ~ 480 이고 화면 가장자리에서 16 을 남긴다. 높이는 600 과 남은 공간 중 작은 쪽까지 — 넘치면 본문이 스크롤한다. 트리거와 8 떨어져 아래에 뜨고, 아래 공간이 모자라면 위로, 옆으로 넘치면 화면 안으로 민다. 모서리 20 · 그림자 s3 — 시트 · 대화상자와 달리 딤이 없어 그림자로 뜬다.
공통
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
root | 최소 너비 | 320px | 가용 폭(화면 − 좌우 16)보다 넓지 않게 |
root | 최대 너비 | 480px | 가용 폭까지 |
root | 최대 높이 | 600px | 가용 높이까지 — 넘치면 본문만 스크롤 |
root | 모서리 | radius-r5 20px | |
root | 배경 | color-bg-layer-floating #FFFFFF · 다크 #2D3346 | |
root | 그림자 | shadow-s3 | |
root | 트리거와 거리 | spacing-x2 8px | 트리거와 8 — 화면 가장자리와는 16 을 남기고, 모자라면 반대편 · 옆으로 민다 |
root | z-index | 200 | z-floating — specs/z-index.md L3 |
header | 위 여백 | spacing-x6 24px | |
header | 좌우 여백 | spacing-x6 24px | |
header | 아래 여백 | spacing-x4 16px | |
header | 간격 | spacing-x1_5 6px | 닫기 버튼이 있으면 오른쪽 52 |
title | 글자 | text-t7 20px / 27px | 단어 단위 줄바꿈 |
title | 굵기 | 700 | |
title | 글자·아이콘 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
description | 글자 | text-t4 14px / 19px | |
description | 굵기 | 400 | |
description | 글자·아이콘 | color-fg-neutral-muted #535866 · 다크 #B7BDCC | |
closeButton | 크기 | 52px | 투명 상자 — 보이는 건 아이콘 22 |
closeButton | 모서리 | radius-r3 12px | |
closeButton | 아이콘 크기 | 22px | |
closeButton | 색 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | |
closeButton | 위 거리 | 27px | 아이콘이 위에서 27 · 오른쪽 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 | 넘칠 수 있는 본문은 끝 흐림(Scroll Fog overlayBody — 늘 켜진 위 20 · 아래 80, 본문 안 여백도 그만큼 · 2026-10-03) · 위로 스크롤하면 머리 아래 1px stroke-neutral-subtle — Dialog 와 같다(dialog.yaml scrollFog) |
notificationList | 너비 | 380px | 벨 아래 — 머리 "알림" · 설명 "읽지 않은 알림 N개" · 목록 · 바닥 "알림 설정" · "모두 읽음"(2026-10-16 사용자 결정 Q-017-3b B) |
notificationList | 최대 높이 | 600px | 이 스펙의 최대 높이 그대로 — 넘치면 목록(본문)만 스크롤 · 끝 흐림. 지금 제품은 min(640, 화면 − 80) |
notificationList | 정렬 | 오른쪽 끝 | 벨 오른쪽 끝에 맞추고 화면 끝 16 을 남긴다(align end) |
notificationList | below1280 | Bottom Sheet | 768 ~ 1279 는 같은 내용을 Bottom Sheet 로 — 이 스펙의 1280 경계 그대로. 폰(768 미만)의 벨은 알림 화면 |
notificationList | rowPress | 읽음 표시 | 줄을 누르면 읽음으로 — 이동하지 않는다(지금 그대로) |
notificationList | rowMore | Button ghost neutralSubtle iconOnly xsmall 32 | 줄 끝 ⋮(ellipsis-vertical) — 보이는 32 · 누르는 44, 이름 "{알림 제목} 더보기". 시각 옆에 두고 줄의 읽음 버튼과 형제다(줄 버튼 안에 넣지 않는다). 폰 알림 화면 · 벨 시트 · 벨 팝오버 모두(2026-10-22 사용자 결정 — 알림 지우기 B) |
notificationList | rowDelete | Menu → Alert Dialog | ⋮ → 1280 미만 Menu Sheet(제목 = 알림 제목) · 이상 Menu 의 위험 줄 "삭제" 하나 → 메뉴가 닫힌 뒤 Alert Dialog "알림 삭제" · "삭제한 알림은 되돌릴 수 없어요." [취소][삭제]. 지우기는 확인에서만 나간다. 실패하면 확인창은 열린 채 두고 웹 · 앱의 오류 알림을 띄운다. 줄 밀기는 두지 않는다 |
footer | 위 여백 | spacing-x4 16px | |
footer | 좌우 여백 | spacing-x6 24px | |
footer | 아래 여백 | spacing-x6 24px | |
footer | 간격 | spacing-x2 8px | |
footer | 가로 맞춤 | flex-end | |
footer | 버튼 크기 | small 36 | Button small |
State
| 상태 | 모습 |
|---|---|
enabled | 열림 |
pressed | 닫기 버튼 — bg-layer-floating-pressed + 2px 거리 축소 |
focused | 키보드 포커스에만 링 2px · 띄움 2px |
상태
| 상태 | closeButton 배경 | closeButton 배율 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|
enabled 열림 | — | — | — | — | — |
pressed 닫기 버튼 누름 | color-bg-layer-floating-pressed #F7F8FD · 다크 #353B4D | 2px 거리 축소 — 기준 52 — 모션 줄이기면 하지 않는다 | — | — | — |
focused 웹 — 키보드 포커스에만 링 | — | — | 2px | 2px | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
열림 | motion-duration-d3 150ms | motion-ease-enter cubic-bezier(0, 0, 0.15, 1) | scale 0.95 → 1, opacity 0 → 1 | 트리거 쪽 변을 기준점으로 커진다 |
닫힘 | motion-duration-d2 100ms | motion-ease-exit cubic-bezier(0.35, 0, 1, 1) | scale → 0.95, opacity → 0 | |
누름 — 닫기 버튼 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale |
Guidelines
트리거에 붙는 짧은 것
팝오버는 그 트리거에 대한 것만 담는다 — 칸 옆 안내 · 그 칸에 넣을 값 고르기(달력 · 시각 · 아이콘 격자 · 목록) · 짧은 동작, 그리고 그 버튼이 여는 목록 하나(Desk 데스크톱 머리 벨의 알림 목록 — 아래). 같은 내용이 1280 미만에서는 Bottom Sheet 로 뜬다(Input Button 이 폭으로 고른다). 폼 · 상세는 Dialog, 되돌릴 수 없는 확인은 Alert Dialog, 줄의 동작 목록은 Menu 다. 어느 떠 있는 표면을 쓸지 일곱을 견준 표는 DESIGN.md Components 의 "시트 · 대화상자 · 확인창 · 팝오버" 한 벌이다(2026-10-22 사용자 결정 3C — https://claude.ai/artifact/YRPbWzhgD4EDdKsVioU8Zj).
폼을 두지 않는 까닭 — 팝오버는 비모달이라 바깥 누르기 · Tab 으로 닫히고, 닫히면 입력이 사라진다. "바뀐 값이 있으면 닫기 전에 묻는다"(Field)와 함께 서지 못한다. 그래서 저장해야 남는 입력은 칸 하나라도 Dialog 다(1280 미만은 같은 폼이 시트로). 고르면 바로 들어가는 것(달력 · 시각 · 목록 · 아이콘)만 팝오버에 둔다 — 고르는 순간 값이 칸에 들어가 잃을 것이 없다(2026-10-22 사용자 결정 10C — https://claude.ai/artifact/YRPbWzhgD4EDdKsVioU8Zj). 옛 스펙(d593204^)은 "짧은 form(2–3 fields) 전용 — 4+ fields 는 dialog" 로 칸 수로 갈랐다 — 칸 수가 아니라 닫히면 잃는지가 경계라 이 경계는 기록으로만 남긴다.
비모달 — 뒤 화면을 막지 않는다
열면 초점이 팝오버 안으로 가지만 가두지 않는다 — 마지막 칸에서 Tab 을 누르면 페이지로 나가고 팝오버는 닫힌다. 딤 · 스크롤 잠금이 없고 뒤 화면을 숨기지 않는다. Esc 로 닫으면 초점이 트리거로 돌아가고, 바깥을 눌러 닫으면 누른 자리로 간다(그 자리가 초점을 받지 못하면 트리거로).
알림 목록 — 벨 아래(1280 이상) · 시트(미만)
Desk 데스크톱 머리의 벨을 누르면 1280 이상에서는 벨 아래 팝오버로, 768 ~ 1279 에서는 Bottom Sheet 로 알림 목록을 연다 — 이 스펙의 1280 경계 그대로다(2026-10-16 사용자 결정 — 구현 세션 질문 Q-017-3b B). 목록이 길고 개수가 정해지지 않았어도 트리거(벨) 하나가 여는 목록이라 팝오버에 둔다 — 폼 · 상세를 담지 않는다. 폭은 380, 높이는 이 스펙대로 600 과 남은 공간 중 작은 쪽까지이고 넘치면 본문(목록)만 스크롤하며 끝을 흐린다. 머리는 제목 "알림" · 설명 "읽지 않은 알림 N개" · 닫기, 바닥은 "알림 설정" · "모두 읽음"(Button small) — 시트도 같은 내용이다(시트의 바닥 버튼은 large 반씩). 줄을 누르면 읽음으로 표시한다(지금 그대로 — 이동하지 않는다). 폰(768 미만)의 벨은 알림 화면으로 간다.
줄 끝에는 ⋮ 하나를 둔다 — Button ghost · ghostColor="neutralSubtle" · iconOnly · xsmall(보이는 32 · 누르는 44), 이름 "{알림 제목} 더보기". 시각 옆에 놓고 줄의 읽음 버튼과 형제다 — 줄 버튼 안에 넣지 않아 따로 눌리고 따로 읽힌다. 누르면 1280 미만은 Menu Sheet(제목 = 알림 제목), 이상은 Menu 로 위험 줄 "삭제" 하나를 연다. 메뉴가 다 닫힌 뒤 Alert Dialog "알림 삭제" · "삭제한 알림은 되돌릴 수 없어요." [취소][삭제] 를 열고, 지우기는 확인에서만 나간다. 지우다 실패하면 확인창을 열어 둔 채 웹 · 앱의 오류 알림을 띄운다 — 다시 누르거나 취소할 수 있다. 줄 밀기로 지우지 않는다. 폰의 알림 화면 · 벨 시트 · 벨 팝오버가 모두 같다(2026-10-22 사용자 결정 — 알림 지우기 B, https://claude.ai/artifact/RXwXuA8Xm5oYXqjGafNvjU).
알림 목록
| 속성 | 값 | 비고 |
|---|---|---|
| 너비 | 380px | 벨 아래 — 머리 "알림" · 설명 "읽지 않은 알림 N개" · 목록 · 바닥 "알림 설정" · "모두 읽음"(2026-10-16 사용자 결정 Q-017-3b B) |
| 최대 높이 | 600px | 이 스펙의 최대 높이 그대로 — 넘치면 목록(본문)만 스크롤 · 끝 흐림. 지금 제품은 min(640, 화면 − 80) |
| 정렬 | 오른쪽 끝 | 벨 오른쪽 끝에 맞추고 화면 끝 16 을 남긴다(align end) |
| below1280 | Bottom Sheet | 768 ~ 1279 는 같은 내용을 Bottom Sheet 로 — 이 스펙의 1280 경계 그대로. 폰(768 미만)의 벨은 알림 화면 |
| rowPress | 읽음 표시 | 줄을 누르면 읽음으로 — 이동하지 않는다(지금 그대로) |
| rowMore | Button ghost neutralSubtle iconOnly xsmall 32 | 줄 끝 ⋮(ellipsis-vertical) — 보이는 32 · 누르는 44, 이름 "{알림 제목} 더보기". 시각 옆에 두고 줄의 읽음 버튼과 형제다(줄 버튼 안에 넣지 않는다). 폰 알림 화면 · 벨 시트 · 벨 팝오버 모두(2026-10-22 사용자 결정 — 알림 지우기 B) |
| rowDelete | Menu → Alert Dialog | ⋮ → 1280 미만 Menu Sheet(제목 = 알림 제목) · 이상 Menu 의 위험 줄 "삭제" 하나 → 메뉴가 닫힌 뒤 Alert Dialog "알림 삭제" · "삭제한 알림은 되돌릴 수 없어요." [취소][삭제]. 지우기는 확인에서만 나간다. 실패하면 확인창은 열린 채 두고 웹 · 앱의 오류 알림을 띄운다. 줄 밀기는 두지 않는다 |
머리는 있을 때 제목 + 닫기
안내 팝오버는 머리에 제목과 닫기 버튼을 둔다. 고르는 패널(달력 · 목록)은 머리 없이 본문만 — 무엇을 고르는지는 트리거가 말한다. 바닥 버튼은 고른 것을 넣을 때("완료")만.
글
제목은 무엇에 대한 안내인지("연차 사용 규정"), 본문은 해요체 짧은 문장. 긴 안내 · 단계가 있는 설명은 팝오버가 아니라 페이지 · 도움말이다.
코드
레시피 recipes/shadcn/components/ui/popover.tsx 를 쓴다(Radix Popover 위). Input Button 의 고르는 패널은 useInputButtonSurface() 가 1280 이상일 때만 팝오버로 연다(Input Button). 아래 미리보기는 스펙 값으로 그린 모습이다.
안내
남은 연차 11일 · 올해 쓴 연차 4일
import { Popover, PopoverBody, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
{/* 1280 이상 — 1280 미만에서는 같은 내용을 Bottom Sheet 로 띄운다(useInputButtonSurface() 로 고른다) */}
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="xsmall" layout="iconOnly" aria-label="연차 사용 규정"><Info /></Button>
</PopoverTrigger>
<PopoverContent title="연차 사용 규정">
<PopoverBody>입사 1년 미만은 한 달에 1일씩 생기고, 1년이 지나면 15일이 생겨요.</PopoverBody>
</PopoverContent>
</Popover>알림 목록 — 1280 이상 팝오버 · 미만 시트
import { Bell } from "lucide-react"
import { BottomSheet, BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetTrigger } from "@/components/ui/bottom-sheet"
import { Button } from "@/components/ui/button"
import { useInputButtonSurface } from "@/components/ui/input-button"
import { Popover, PopoverBody, PopoverContent, PopoverFooter, PopoverTrigger } from "@/components/ui/popover"
import { TopNavigationIconButton } from "@/components/ui/top-navigation"
const surface = useInputButtonSurface() // 1280 미만 "sheet" · 이상 "popover"
const bell = (
<TopNavigationIconButton notification={unread > 0} aria-label={unread > 0 ? "알림, 새 알림 있음" : "알림"}>
<Bell />
</TopNavigationIconButton>
)
const description = unread > 0 ? `읽지 않은 알림 ${unread}개` : "새 알림이 없어요"
{/* 같은 내용 — 넓으면 벨 아래 팝오버(폭 380), 좁으면 아래 시트. 줄을 누르면 읽음 표시 */}
{surface === "popover" ? (
<Popover>
<PopoverTrigger asChild>{bell}</PopoverTrigger>
<PopoverContent title="알림" description={description} align="end" className="w-[380px]">
<PopoverBody scrollFog>{notificationList}</PopoverBody>
<PopoverFooter>
<Button variant="neutralWeak" size="small" onClick={openNotificationSettings}>알림 설정</Button>
<Button variant="neutralSolid" size="small" onClick={markAllRead} disabled={unread === 0}>모두 읽음</Button>
</PopoverFooter>
</PopoverContent>
</Popover>
) : (
<BottomSheet>
<BottomSheetTrigger asChild>{bell}</BottomSheetTrigger>
<BottomSheetContent title="알림" description={description}>
<BottomSheetBody scrollFog>{notificationList}</BottomSheetBody>
<BottomSheetFooter>
<Button variant="neutralWeak" onClick={openNotificationSettings}>알림 설정</Button>
<Button variant="neutralSolid" onClick={markAllRead} disabled={unread === 0}>모두 읽음</Button>
</BottomSheetFooter>
</BottomSheetContent>
</BottomSheet>
)}Behavior
| 인터랙션 | 동작 |
|---|---|
| 트리거 누르기 | 열고 닫는다. 150ms 로 트리거 쪽에서 커지며 나타난다. |
| 열린 뒤 | 초점이 팝오버 안으로(머리 닫기 버튼이 아니라 내용) 간다. |
| Tab | 팝오버 안을 지나 마지막에서 페이지로 나가면 닫힌다(초점은 나간 자리에 남는다). |
| Shift+Tab | 거꾸로 — 내용 첫 칸 다음은 머리 닫기 버튼, 그다음 트리거(팝오버는 열린 채). 보이는 순서(닫기는 위)와 같고, 닫기 버튼에 키보드로 닿는 길이 남는다. 2026-10-15 사용자 결정 — 구현 세션 질문 Q-006 |
Esc | 닫고, 초점은 트리거로 돌아간다. |
| 바깥 누르기 | 닫는다 — 초점은 누른 자리로 간다(누른 곳이 초점을 받지 못하면 트리거로). |
| 대화상자 · 시트 안에서 | 그 위에 뜬다(z-index L3) — 대화상자가 닫히면 함께 닫힌다. |
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) | 표면 경계는 그림자 s3(딤 없음) — 표면을 알리는 유일한 표시가 아니다(머리 · 내용이 알린다). 키보드 포커스 링 Desk 8.38 · 5.28 · HR 8.17 · 5.26 ✓ |
| WCAG 1.4.13 Content on Hover or Focus | 누를 때 열리고(호버로 열지 않는다) 바깥 · Esc 로 닫힌다 ✓ |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 닫기 버튼 52 ✓ · 알림 줄 ⋮ 보이는 32(누르는 44) ✓ |
| ARIA | role="dialog"(aria-modal 없음), 제목 aria-labelledby(있을 때), 트리거 aria-haspopup="dialog" · aria-expanded · aria-controls. 머리 없는 고르는 패널은 aria-label 로 이름을 단다 |
Do / Don't
✅ Do
- 트리거에 대한 짧은 안내 · 값 고르기 · 그 버튼이 여는 목록(벨의 알림)을 1280 이상에서 팝오버로 — 1280 미만은 같은 내용을 시트로.
- 열면 초점을 안으로, 닫으면 트리거로.
- 안내 팝오버는 제목 + 닫기.
❌ Don't
- 팝오버에 폼 · 상세(Dialog) — 저장해야 남는 입력은 칸 하나라도. 바깥 누르기 ·
Tab으로 닫혀 입력이 사라진다. - 호버로 열기(도움말 말풍선은 Help Bubble — 그 차례에).
- 팝오버 위에 팝오버(고르는 패널 안의 작은 패널은 예외 — 달력의 월 · 연도).
- 1280 미만에서 팝오버(시트).
Specification
popover.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Popover 를 만들 때 이 값을 그대로 쓴다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 최대 높이maxHeight | 600px가용 높이까지 — 넘치면 본문만 스크롤 |
| 최소 너비minWidth | 320px가용 폭(화면 − 좌우 16)보다 넓지 않게 | ||
| 최대 너비maxWidth | 480px가용 폭까지 | ||
| 모서리radius | radius-r520px | ||
| 배경background | bg-layer-floating#FFFFFF다크 #2D3346 | ||
| 그림자shadow | shadow-s30 8px 24px -4px rgba(15, 18, 28, 0.10), 0 2px 6px -2px rgba(15, 18, 28, 0.05) · 다크 0 8px 24px -4px rgba(0, 0, 0, 0.50), 0 2px 6px -2px rgba(0, 0, 0, 0.25), inset 0 1px 0 0 rgba(255, 255, 255, 0.08) | ||
| z-indexzIndex | 200z-floating — specs/z-index.md L3 | ||
| 트리거와 거리offset | spacing-x28px트리거와 8 — 화면 가장자리와는 16 을 남기고, 모자라면 반대편 · 옆으로 민다 | ||
closeButton | enabled | 크기size | 52px투명 상자 — 보이는 건 아이콘 22 |
| 아이콘 크기iconSize | 22px | ||
| 모서리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 | 27px아이콘이 위에서 27 · 오른쪽 24 — 제목 첫 줄 가운데와 맞는다 | ||
| 오른쪽 거리right | spacing-x624px | ||
header | enabled | 좌우 여백paddingX | spacing-x624px |
| 위 여백paddingTop | spacing-x624px | ||
| 아래 여백paddingBottom | spacing-x416px | ||
| 간격gap | spacing-x1_56px닫기 버튼이 있으면 오른쪽 52 | ||
title | enabled | 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA |
| 글자typography | text-t720px / 27px단어 단위 줄바꿈 | ||
| 굵기fontWeight | 700 | ||
description | enabled | 글자·아이콘foreground | fg-neutral-muted#535866다크 #B7BDCC |
| 글자typography | text-t414px / 19px | ||
| 굵기fontWeight | 400 | ||
body | enabled | 좌우 여백paddingX | spacing-x624px |
| 위 여백paddingTop | spacing-x624px머리가 없을 때만 — 머리가 있으면 0 | ||
| 아래 여백paddingBottom | spacing-x624px바닥이 없을 때만 — 바닥이 있으면 0 | ||
| 세로 넘침overflowY | auto넘칠 수 있는 본문은 끝 흐림(Scroll Fog overlayBody — 늘 켜진 위 20 · 아래 80, 본문 안 여백도 그만큼 · 2026-10-03) · 위로 스크롤하면 머리 아래 1px stroke-neutral-subtle — Dialog 와 같다(dialog.yaml scrollFog) | ||
footer | enabled | 버튼 크기buttonSize | small 36Button small |
| 좌우 여백paddingX | spacing-x624px | ||
| 위 여백paddingTop | spacing-x416px | ||
| 아래 여백paddingBottom | spacing-x624px | ||
| 간격gap | spacing-x28px | ||
| 가로 맞춤justifyContent | flex-end | ||
notificationList | enabled | 최대 높이maxHeight | 600px이 스펙의 최대 높이 그대로 — 넘치면 목록(본문)만 스크롤 · 끝 흐림. 지금 제품은 min(640, 화면 − 80) |
| 너비width | 380px벨 아래 — 머리 "알림" · 설명 "읽지 않은 알림 N개" · 목록 · 바닥 "알림 설정" · "모두 읽음"(2026-10-16 사용자 결정 Q-017-3b B) | ||
| 정렬align | 오른쪽 끝벨 오른쪽 끝에 맞추고 화면 끝 16 을 남긴다(align end) | ||
| below1280below1280 | Bottom Sheet768 ~ 1279 는 같은 내용을 Bottom Sheet 로 — 이 스펙의 1280 경계 그대로. 폰(768 미만)의 벨은 알림 화면 | ||
| rowPressrowPress | 읽음 표시줄을 누르면 읽음으로 — 이동하지 않는다(지금 그대로) | ||
| rowMorerowMore | Button ghost neutralSubtle iconOnly xsmall 32줄 끝 ⋮(ellipsis-vertical) — 보이는 32 · 누르는 44, 이름 "{알림 제목} 더보기". 시각 옆에 두고 줄의 읽음 버튼과 형제다(줄 버튼 안에 넣지 않는다). 폰 알림 화면 · 벨 시트 · 벨 팝오버 모두(2026-10-22 사용자 결정 — 알림 지우기 B) | ||
| rowDeleterowDelete | Menu → Alert Dialog⋮ → 1280 미만 Menu Sheet(제목 = 알림 제목) · 이상 Menu 의 위험 줄 "삭제" 하나 → 메뉴가 닫힌 뒤 Alert Dialog "알림 삭제" · "삭제한 알림은 되돌릴 수 없어요." [취소][삭제]. 지우기는 확인에서만 나간다. 실패하면 확인창은 열린 채 두고 웹 · 앱의 오류 알림을 띄운다. 줄 밀기는 두지 않는다 | ||
closeButton | pressed | 배경background | bg-layer-floating-pressed#F7F8FD다크 #353B4D |
| 배율scale | 2px 거리 축소기준 52 — 모션 줄이기면 하지 않는다 | ||
focusRing | focused | 링 두께width | 2px |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | 2px |
SEED 와 다른 점
- 바닥 버튼은 Button small 36 — SEED 예제는 medium 40. Button 결정을 따랐다.
- 반투명 색을 불투명 짝으로(v102) — 닫기 버튼 누름
bg-layer-floating-pressed. - 1280 미만에서는 쓰지 않는다 — 같은 내용을 Bottom Sheet 로(Input Button 의 경계). SEED Popover 는 폭 규칙이 없다.
- z-index 는 specs/z-index.md 의 L3(
z-floating200) — SEED 99999. 대화상자 · 시트(L2) 위, 확인창(L5) 아래.
Migration notes
2026-10-22 — 알림 줄 ⋮ 와 지우기(알림 지우기 B)
사용자가 알림 지우기 비교 페이지에서 정했다 — 줄 끝 ⋮ → "삭제" → 확인창, 웹 · 앱 같게(B). 웹 폰 알림 줄의 × 를 걷자 웹에서 알림을 지울 길이 없었고(데스크톱 벨에는 원래 없었다), 앱은 줄마다 × 로 확인 없이 바로 지웠다. 앱의 × 는 걷는다. 거절: 지우기 없음(A) · 폰은 밀기 + ⋮(C) · 편집 → 골라 삭제 · 모두 삭제(D). I-04 검수 뒤 디자인 세션이 맞춘 것 — ⋮ 는 Button xsmall(32) neutralSubtle(앱은 small 이었다), 지우기 실패는 확인창을 남기고 오류 알림(앱은 조용히 닫혔다).
2026-10-22 — 폼을 두지 않는 까닭과 경계
"폼 · 상세는 Dialog" 금지는 그대로 두고 까닭(비모달 — 닫히면 입력이 사라진다)과 경계(저장해야 남는 입력은 칸 하나라도 Dialog, 고르면 바로 들어가는 것만 팝오버)를 적었다. 옛 경계(d593204^ — "2–3 fields 까지 · 4+ fields 는 Dialog")는 기록으로만 남긴다(사용자 결정 10C — https://claude.ai/artifact/YRPbWzhgD4EDdKsVioU8Zj). 값 변경 없음.
2026-10-16 — 알림 목록(Q-017-3b B)
사용자가 구현 질문 Q-017 · Q-018 비교 페이지에서 정했다 — Desk 데스크톱 벨의 알림 목록은 1280 이상 팝오버 · 768 ~ 1279 Bottom Sheet(B). 지금 팝오버를 768 이상 모두에 두기(A — 1280 규칙의 예외) · 알림 화면으로 가기(C)는 고르지 않았다. "트리거에 대한 것만" 문장에 벨이 여는 목록을 더했다.
- Desk 웹 —
widgets/notification-manage/ui/NotificationsPopover.tsx는 화면 고정 top 58 · right 20 · 380 × min(640, 화면 − 80), z-index auto ·Esc로 안 닫힘이다. 1280 이상은 이 Popover 부품(벨 아래 8 · 화면 끝 16 · 높이 600 까지 ·Esc· 바깥 · z-floating)으로, 768 ~ 1279 는 Bottom Sheet 로 같은 내용을 연다(머리 "알림" · 읽지 않은 알림 N개 · 목록 · "알림 설정" · "모두 읽음").
2026-10-15 — 첫 칸에서 Shift+Tab
구현 세션 질문 Q-006 을 사용자가 정했다 — 지금대로 내용 첫 칸 → 머리 닫기 버튼 → 트리거. Behavior 에 줄을 더했다(레시피 · 웹은 이미 그렇다). 바로 트리거로 가는 안(닫기 버튼에 키보드로 못 닿는다) · 닫기 버튼을 DOM 뒤로 옮기는 안은 고르지 않았다.
2026-10-02 — SEED Popover 로 다시 정한다
사용자가 비교 페이지에서 SEED 모양을 골랐다 — 폭 320 ~ 480 · 최대 높이 600 · 모서리 20 · 그림자 s3 · 트리거와 8 · 머리 제목 20 / 27 + 닫기 · 비모달(초점은 안으로, 가두지 않음). 옛 Popover(288 · 모서리 8 · 1px 테두리 · shadow-md · 안쪽 12)는 걷었다 — 옛 스펙은 popover.history/v-pre-seed-overlay.*.
제품은 앱 적용 단계에서 옮긴다(2026-10-02 조사).
- Desk 웹 — Popover 부품 6종(날짜 14 · 시각 6 · 아이콘 2 · 캘린더 3)이 360 폭에서도 팝오버로 뜬다(→ 1280 미만은 시트). 날짜 팝오버는 이름이 없고 첫 초점이 영어 "Go to the Previous Month" 버튼이다. 손으로 만든 월 고르기(데스크톱 5곳) · 알림 팝오버는 z-index 가 auto 라 다른 층 아래로 들어가고
Esc로 닫히지 않는다. - HR 웹 — Popover 14곳(규정 안내 12 · 일정 필터 · 역할 고르기) + 날짜 · 시각 피커 21곳. 규정 안내는 이름이 없다.
- 앱 적용 때 화면마다 정할 자리 — HR 규정 안내 12(Popover · Help Bubble), 데스크톱 필터(Popover · Dialog · Side Panel). 알림 팝오버는 2026-10-16 정했다(위).
Pagination
여러 쪽으로 나뉜 목록의 쪽을 오가는 줄 — 쪽 단위로 훑는 목록(주로 데스크톱 768 이상) 아래 가운데에 둔다. 몇 쪽인지 한눈에 보이고, 쪽이 바뀌어도 칸 자리가 흔들리지 않는다. 폰(768 미만)의 긴 목록은 쪽을 나누지 않고 끝없이 불러온다(아래 "폰은 끝없이 불러오기"). 데이터 표는 Table Pagination 이다.
Progress Circle
작업이 진행 중임을 알리는 원 — 값을 모르면 호가 늘었다 줄며 돌고, 값을 알면 12시부터 값만큼 채운다. 섹션 하나를 새로 고칠 때 · 목록 끝을 더 불러올 때 · 구조를 미리 그릴 수 없는 화면을 기다릴 때 · 올리기 · 받기에 쓴다. 목록 · 카드처럼 구조가 보이는 자리를 처음 불러올 때는 Skeleton, 저장 버튼은 Button 의 로딩이다. 앱의 당겨서 새로 고침도 이 원이다(아래 "당겨서 새로 고침").