Menu
트리거에 붙어 열리는 동작 목록 — 줄을 누르면 바로 실행하고 닫힌다. 1280 이상에서 쓰고, 같은 목록을 1280 미만에서는 Menu Sheet 로 띄운다. 값을 고르는 일(테마 · 정렬 · 보기)은 Menu 가 아니라 Select · Segmented Control · Radio 다.
구조는 당근 SEED Menu(Apache-2.0)를 따른다 — 메뉴 · 묶음 · 묶음 이름 · 줄(앞 아이콘 · 이름 · 설명 · 뒤 아이콘), 묶음 사이에만 선. 크기는 SEED small 하나다(1280 미만은 Menu Sheet 가 맡는다). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-02 사용자 결정). 옛 Dropdown Menu 를 대신하고, Context Menu · Menubar 는 걷었다.
수치 원본은 menu.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
아이콘 · 설명 · 묶음 이름 · 위험한 동작 · 막힌 줄을 고르면 스펙대로 그린 메뉴와 그 코드가 바뀐다. 실제로 열고, 키보드로 옮기고, 눌러 닫을 수 있다.
⋮ 를 누르거나, Tab 으로 옮긴 뒤 ↓ 로 열어 보세요.
import { Menu, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuTrigger } from "@/components/ui/menu"
<Menu>
<MenuTrigger asChild>
<Button variant="ghost" layout="iconOnly" aria-label="점심 식사 더보기">
<EllipsisVertical />
</Button>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem icon={<Pencil />} label="수정" onSelect={edit} />
<MenuItem icon={<Copy />} label="복사해 새로 쓰기" onSelect={duplicate} />
</MenuGroup>
<MenuGroup>
<MenuGroupLabel>정산</MenuGroupLabel>
<MenuItem icon={<Undo2 />} label="환불 기록" description="돌려받은 돈을 붙여요." onSelect={recordRefund} />
<MenuItem icon={<Users />} label="나눠 내기" onSelect={splitBill} />
</MenuGroup>
<MenuGroup>
<MenuItem icon={<Trash2 />} label="삭제" tone="critical" onSelect={askDelete} />
</MenuGroup>
</MenuContent>
</Menu>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Content | 메뉴 — 트리거 아래 8(모자라면 위 · 옆). 폭 200, 글이 길면 줄을 바꾼다. |
| ⓑ Group | 묶음 — 줄 여럿. 묶음 사이에만 선을 긋는다. |
| ⓒ Group Label | 묶음 이름 — 있을 때만. 누를 수 없다. |
| ⓓ Divider | 선 — 묶음 사이에만. 줄 사이에는 없다. |
| ⓔ Item | 줄 — 앞 아이콘 · 이름 · 설명 · 뒤 아이콘. 누르면 실행하고 닫힌다. |
| ⓕ Highlight | 호버 · 누름 바탕 — 줄 폭 전체에서 좌우 8 들어오며 둥글어지는 알약. 키보드 포커스 링은 다 들어온 자리(좌우 8 · 모서리 12)에 그린다. |
부위
| 부위 | 설명 |
|---|---|
content | 메뉴 — 트리거 아래(모자라면 위 · 옆) 8 |
group | 묶음 — 묶음 사이에만 선을 긋는다 |
groupLabel | 묶음 이름 — 있을 때만, 누를 수 없다 |
divider | 묶음 사이 선 — 줄 사이에는 없다 |
item | 줄 — 누르면 실행하고 닫힌다. 글은 메뉴 안에서 줄바꿈된다 |
itemIcon | 앞 아이콘 — 쓰면 모든 줄에 |
itemLabel | 이름 — 동사로 짧게(2 ~ 6자) |
itemDescription | 설명 — 한 줄, 꼭 필요한 줄에만 |
suffixIcon | 뒤 아이콘 — 바깥 링크처럼 방향을 알릴 때만 |
highlight | 호버 · 누름 바탕 — 좌우 8 들인 알약 |
focusRing | 키보드 포커스 링 — 알약 자리에(웹) |
Properties
줄 · 묶음 · 선
줄은 위아래 10 · 좌우 16 이고 이름 14 / 19, 아이콘 18, 아이콘 ↔ 이름 8 이다 — 한 줄이면 39, 설명이 있으면 57. 글이 길면 말줄임 없이 줄을 바꾼다(단어 단위). 메뉴는 위아래 8 을 두고, 묶음 사이는 8 + 선 1 + 8 이다. 선은 좌우 16 들인다.
메뉴 폭 200 · 위아래 8 · 모서리 20 — 줄 위아래 10 · 좌우 16, 아이콘 18(분홍) · 사이 8, 이름 14 / 19, 설명 12 / 16(사이 2) → 한 줄 39 · 설명이 있으면 57
묶음 사이 8 + 선 1 + 8 · 선 좌우 16 들임 — 알약 좌우 8 들임 · 모서리 12(삭제 줄 — 마우스가 올라간 모습) · 묶음 이름 13 / 18 · 위아래 8 → 34
공통
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
content | 너비 | 200px | 글이 길면 줄을 바꾼다 — 폭을 늘리지 않는다 |
content | 배경 | color-bg-layer-floating #FFFFFF · 다크 #2D3346 | |
content | 모서리 | radius-r5 20px | |
content | 그림자 | shadow-s3 | |
content | 상하 여백 | spacing-x2 8px | |
content | 트리거와 거리 | spacing-x2 8px | 트리거와 8 — 화면 가장자리와도 8(Menu 는 1280 이상이라 안전 영역이 없다) |
content | 정렬 | end | 트리거의 오른쪽 끝에 맞춘다(머리 ⋮ · 표 줄 끝 ⋮) — 왼쪽에 놓인 트리거는 start. 모자라면 화면 안으로 민다 |
content | 최대 높이 | min(480px, 남은 화면) | 남은 화면이 200 보다 좁아도 200 은 둔다 — 먼저 줄이고, 200 아래로 내려가야 반대편으로 뒤집는다. 넘치면 메뉴 안 스크롤 |
content | z-index | 200 | z-floating — specs/z-index.md L3. 페이지에서도, 대화상자 · 시트 안에서도 |
item | 상하 여백 | spacing-x2_5 10px | |
item | 좌우 여백 | spacing-x4 16px | |
item | 간격 | spacing-x2 8px | |
item | 높이 | 39px | 한 줄 — 설명이 있으면 57. 글이 길면 줄을 바꿔 늘어난다(말줄임 없음) |
item | 커서 | pointer | |
item | 전환 대상 | background-color, inset, border-radius | 알약 바탕 · 들임(0 → 8) · 모서리(0 → 12)가 함께 150ms 에 — SEED menu-item 은 들임 · 모서리 d3, 색 color-transition(둘 다 150ms) 이라 하나로 적었다(2026-10-15 사용자 결정 — 구현 세션 질문 Q-009-3, 안 B) |
item | 전환 시간 | motion-duration-color-transition 150ms | |
item | 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | |
itemIcon | 크기 | 18px | lucide · 여러 줄이면 세로 가운데 |
itemIcon | 색 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
itemLabel | 글자 | text-t4 14px / 19px | 단어 단위 줄바꿈(v114) |
itemLabel | 굵기 | 400 | |
itemLabel | 글자·아이콘 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
itemDescription | 글자 | text-t2 12px / 16px | |
itemDescription | 굵기 | 400 | |
itemDescription | 글자·아이콘 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | |
itemDescription | 간격 | spacing-x0_5 2px | 이름 ↔ 설명 |
suffixIcon | 뒤 아이콘 | 16px | |
suffixIcon | 뒤 아이콘 색 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
highlight | 배경 | transparent | 마우스를 올리거나 누를 때만 칠한다 |
highlight | 바탕 들임 | 0px | 줄 폭 전체 — 마우스를 올리거나 누르면 8 로 들어온다(SEED menu-item enabled 에는 marginX 가 없다) |
highlight | 모서리 | 0px | 모서리 없음 — 들어오며 12 로 둥글어진다 |
groupLabel | 글자 | text-t3 13px / 18px | |
groupLabel | 굵기 | 400 | |
groupLabel | 글자·아이콘 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | |
groupLabel | 상하 여백 | spacing-x2 8px | |
groupLabel | 좌우 여백 | spacing-x4 16px | |
divider | 색 | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D | |
divider | 높이 | 1px | |
divider | 좌우 바깥 여백 | spacing-x4 16px | 좌우 16 들인다 |
divider | 상하 바깥 여백 | spacing-x2 8px | 묶음 사이 8 + 선 1 + 8 |
Tone
되돌릴 수 없는 동작(삭제 · 나가기)은 critical — 이름과 아이콘만 빨갛고 설명은 그대로다. 맨 아래 묶음에 따로 둔다.
위험한 동작
| tone | itemIcon 색 | itemLabel 글자·아이콘 | 뒤 아이콘 색 | 설명 |
|---|---|---|---|---|
neutral (기본) | — | — | — | 대부분의 동작 |
critical | color-fg-critical #D72323 · 다크 #FF8477 | color-fg-critical #D72323 · 다크 #FF8477 — 설명은 fg-neutral-subtle 그대로 | color-fg-critical #D72323 · 다크 #FF8477 | 되돌릴 수 없는 동작(삭제 · 나가기) — 맨 아래 묶음 |
State
| 상태 | 모습 |
|---|---|
enabled | 바탕 없음 |
hovered | 웹 — 마우스를 올린 줄에 알약 바탕(누름과 같은 색). 키보드 위치는 따로 움직인다 |
pressed | 알약 바탕 + 아이콘 · 글만 2px 거리 축소 |
focused | 웹 — 키보드로 옮긴 줄에 알약 자리 2px 링. 바탕은 칠하지 않는다 |
disabled | 전용 색 — 눌러도 실행하지 않고 닫히지 않는다. 화살표 키가 건너뛴다 |
호버와 키보드 포커스를 다르게 그린다 — 마우스가 한 줄 위에 있고 키보드 위치가 다른 줄에 있으면 둘 다 보인다.
알약은 제자리에서 색만 나타나지 않는다 — 마우스를 올리거나 누르면 줄 폭 전체에서 좌우 0 → 8 들어오며 모서리 0 → 12 로 둥글어지고, 색과 함께 150ms 에 끝난다(SEED menu-item 의 pressed marginX x2 · cornerRadius r3, marginDuration · borderRadiusDuration d3). 떠날 때는 거꾸로 펴지며 투명해진다. 키보드 포커스 링은 다 들어온 자리에 고정이라, 들어오는 중인 알약과 잠깐 폭이 다를 수 있다. 2026-10-15 사용자 결정 — 구현 세션 질문 Q-009-3, 안 B(제자리에서 색만 나타나는 안 A 는 고르지 않았다). List 의 누름 바탕 · Searchable List 의 강조 바탕도 같은 움직임이다(들임 6).
상태
| 상태 | item 배율 | item 커서 | item 축소 시간 | item 축소 곡선 | itemIcon 색 | itemLabel 글자·아이콘 | itemDescription 글자·아이콘 | 뒤 아이콘 색 | highlight 바탕 들임 | highlight 모서리 | highlight 배경 | 링 두께 | 링 간격 | 링 색 | focusRing 바탕 들임 | focusRing 모서리 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
enabled 기본 | — | pointer | — | — | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 0px — 줄 폭 전체 — 마우스를 올리거나 누르면 8 로 들어온다(SEED menu-item enabled 에는 marginX 가 없다) | 0px — 모서리 없음 — 들어오며 12 로 둥글어진다 | transparent — 마우스를 올리거나 누를 때만 칠한다 | — | — | — | — | — |
hovered 웹 — 마우스를 올린 줄에 알약 바탕(누름과 같은 색, 축소 없음). 키보드 위치와 따로 움직인다 | — | pointer | — | — | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | spacing-x2 8px — 좌우 8 들어온 알약 — 글은 알약 안쪽 8 지점에서 시작한다(SEED pressed marginX x2) | radius-r3 12px — 12(SEED pressed cornerRadius r3) | color-bg-layer-floating-pressed #F7F8FD · 다크 #353B4D — 메뉴 바탕(bg-layer-floating) 위의 누름 색 | — | — | — | — | — |
pressed 누름 — 알약 바탕 + 내용만 2px 거리 축소 | 콘텐츠 2px 거리 축소 — 아이콘 · 글만 준다(SEED scaleScope: content) — 알약은 그대로. 모션 줄이기면 하지 않는다 | pointer | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | spacing-x2 8px | radius-r3 12px | color-bg-layer-floating-pressed #F7F8FD · 다크 #353B4D | — | — | — | — | — |
focused 웹 — 키보드로 옮긴 줄에 알약 자리 2px 링(바탕 없음) | — | pointer | — | — | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 0px — 줄 폭 전체 — 마우스를 올리거나 누르면 8 로 들어온다(SEED menu-item enabled 에는 marginX 가 없다) | 0px — 모서리 없음 — 들어오며 12 로 둥글어진다 | transparent — 마우스를 올리거나 누를 때만 칠한다 | 2px | -2px — 알약 자리(좌우 8 들임 · 모서리 12) 안쪽에 — 바탕은 칠하지 않는다. 링은 그 자리에 고정이라 움직이지 않는다 — 들어오는 중인 호버 알약과 잠깐 폭이 다를 수 있다 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA | spacing-x2 8px — 알약이 다 들어온 자리 | radius-r3 12px |
disabled 막힌 줄 — 전용 색, 화살표 키가 건너뛴다 | — | not-allowed | — | — | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | 0px — 줄 폭 전체 — 마우스를 올리거나 누르면 8 로 들어온다(SEED menu-item enabled 에는 marginX 가 없다) | 0px — 모서리 없음 — 들어오며 12 로 둥글어진다 | transparent — 호버 · 누름 바탕이 없다 — 눌러도 실행하지 않고 닫히지 않는다 | — | — | — | — | — |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
열림 | 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-color-transition 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | background-color, inset, border-radius | 알약이 줄 폭 전체에서 좌우 0 → 8 들어오며 모서리 0 → 12 로 둥글어진다(SEED menu-item — 2026-10-15 사용자 결정 Q-009-3 안 B). 떠날 때는 거꾸로 펴지며 투명해진다 |
누름 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale |
Guidelines
메뉴에는 실행하는 동작만
Menu 는 누르면 바로 실행되는 명령 목록이다. 값을 고르는 일은 메뉴에 두지 않는다 — 메뉴에는 고른 표시(체크 · 라디오)가 없고, 고른 값을 저장하거나 화면에 반영하는 일은 그 값의 컴포넌트가 한다.
| 이런 일 | 쓰는 것 |
|---|---|
| 들어간 화면 · 표 줄의 동작(수정 · 복사 · 삭제 · 내보내기) — 목록 줄에는 두지 않는다(아래 "줄의 동작") | Menu(1280 미만 Menu Sheet) |
| 폼에 넣을 값 고르기(결제 수단 · 카테고리) | Select · Input Button |
| 같은 내용의 보기 바꾸기 2 ~ 4개(목록 · 달력) | Segmented Control |
| 값 고르기 — 테마처럼 누르는 순간 바뀌는 설정 · 정렬 · 보기 | 테마는 List 의 라디오 줄, 정렬 · 보기는 Segmented Control · Select — 고르는 표는 Select 의 "고르는 컴포넌트 고르기" |
| 되돌릴 수 없는 확인 | 메뉴를 닫은 뒤 Alert Dialog |
줄의 동작 — 목록 줄에는 ⋮ 를 두지 않는다
좁은 목록 줄(List 줄 — 한두 줄 글 + 타일 · 값)에는 줄 끝 ⋮ 를 두지 않는다. 줄을 누르면 한 단계 들어간다(상세 · 수정). 그 항목의 동작은 들어간 화면에 둔다.
| 들어간 화면의 동작 | 두는 자리 |
|---|---|
| 하나(삭제) | 바닥 왼쪽 [삭제] — Button ghost + critical(Button 의 "모달 footer" 꼴). 1280 미만 Bottom Sheet 도 같은 꼴이다 |
| 여럿 | 머리 오른쪽 ⋮ 하나 — 이름 "{항목 이름} 더보기"(Button ghost · iconOnly) → Menu(1280 미만 Menu Sheet) |
- 폰 목록의 밀기는 같은 동작의 지름길로 남는다 — 트레이의 동작은 모두 들어간 화면에도 있다.
- 줄에 ⋮ 를 둘 수 있는 건 콘텐츠가 높은 카드의 머리뿐이다 — 사진 · 여러 줄 · 버튼이 든 카드 하나가 항목 하나일 때(Card 의 "머리 동작은 하나"). 지금 담다 화면에는 이런 카드가 없다.
- 수정 · 삭제 아이콘을 줄마다 늘 보이게 늘어놓지 않는다 — 줄마다 버튼이 둘셋씩 늘고, 아이콘만으로는 뜻을 다 전하지 못한다.
- 표 줄은 이 규칙 밖이다 — 데스크톱 데이터 표(Table)의 줄 끝 ⋮ 는 이번에 정하지 않았다. 그루 이음 관리 화면 차례에 다시 보고, 그때까지 table.md 의 지금 문장을 따른다.
당근도 ⋮ 를 콘텐츠가 높은 카드(구매 내역 카드 · 커뮤니티 글)의 머리에만 두고, 모임 목록 같은 좁은 줄에는 두지 않는다 — 한 단계 들어간 화면의 머리 ⋮ 에서 그 항목의 동작을 고른다. 앞의 규칙(2026-10-02 "데스크톱 줄의 동작은 줄 끝 ⋮ + Menu")은 디자인 시스템 그림을 우리 목록에 그대로 붙인 것이었다(2026-10-25 사용자 결정 — 아래 Migration notes).
묶음과 순서
가장 많이 쓰는 동작을 위에, 위험한 동작은 맨 아래 묶음에 따로 둔다. 줄이 7개를 넘으면 묶음으로 나눈다 — 선은 묶음 사이에만 긋는다. 묶음 이름은 묶음이 무엇인지 말해야 할 때만 둔다.
- 아이콘은 모든 줄에 두거나 모두 뺀다. 아이콘만 두고 이름을 빼지 않는다.
- 앞 아이콘과 뒤 아이콘을 한 줄에 함께 두지 않는다. 뒤 아이콘은 바깥 링크처럼 방향을 알릴 때만.
- 설명은 이름만으로 무엇을 하는지 모를 줄에만 한 줄로 — 모든 줄에 두지 않는다.
1280 에서 Menu Sheet 로
같은 목록이 1280 미만에서는 Menu Sheet 로 뜬다 — 같은 줄 · 같은 순서 · 같은 막힘. 메뉴는 마우스용(줄 39)이고, 손가락으로 누르는 화면은 시트(줄 52)가 맡는다. 코드는 폭을 보고 둘 중 하나를 그리는 ResponsiveMenu 로 짠다.
예외 하나 — 접힌 사이드바(768 ~ 1279)의 하위 목록은 Menu Sheet 로 바꾸지 않고 누른 아이콘 옆 펼침 메뉴(이 표면 · 폭 200 · 줄 44)로 띄운다(Side Navigation — 사용자 결정 2026-10-08).
글
이름은 동사로 짧게(2 ~ 6자) — "수정" · "복사해 새로 쓰기" · "삭제". 한 메뉴 안에서는 말투와 형식을 맞추고, 줄임말 · 영어 섞기를 피한다. 설명은 한 줄 문장(해요체 · 마침표)이다 — "새 비밀번호를 메일로 보내요."
코드
레시피 recipes/shadcn/components/ui/menu.tsx 를 쓴다(Radix DropdownMenu 위, 비모달). 들어간 화면 머리의 ⋮ 처럼 폭마다 바뀌는 자리는 ResponsiveMenu(1280 이상 Menu · 미만 Menu Sheet)로 짜고, 늘 1280 이상인 자리(데스크톱 표의 줄)만 Menu 를 바로 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.
들어간 화면 머리 ⋮ — 1280 에서 Menu Sheet 로 바뀐다
메모 목록 줄을 누르면 메모 화면으로 들어가고, 동작이 여럿(고정 · 수정 · 복사 · 삭제)이라 머리 오른쪽 ⋮ 하나에 모은다.
업무 · 10월 2일
다음 분기 목표를 정리했다.
· 신규 기능 두 가지 출시
· 응답 속도 20% 줄이기
import {
ResponsiveMenu, ResponsiveMenuContent, ResponsiveMenuGroup, ResponsiveMenuItem, ResponsiveMenuTrigger,
} from "@/components/ui/menu"
// 들어간 화면(메모)의 머리 오른쪽 — 목록 줄에는 ⋮ 를 두지 않는다
<ResponsiveMenu>
<ResponsiveMenuTrigger asChild>
<Button variant="ghost" layout="iconOnly" aria-label="주간 회의 메모 더보기">
<EllipsisVertical />
</Button>
</ResponsiveMenuTrigger>
{/* title — Menu Sheet 의 제목(1280 이상 메뉴에는 머리가 없다) */}
<ResponsiveMenuContent title="주간 회의 메모">
<ResponsiveMenuGroup>
<ResponsiveMenuItem icon={<Pin />} label="고정" onSelect={pin} />
<ResponsiveMenuItem icon={<Pencil />} label="수정" onSelect={edit} />
<ResponsiveMenuItem icon={<Copy />} label="복사해 새로 쓰기" onSelect={duplicate} />
</ResponsiveMenuGroup>
<ResponsiveMenuGroup>
{/* 확인이 필요한 동작 — 메뉴가 닫힌 뒤 Alert Dialog 를 연다 */}
<ResponsiveMenuItem icon={<Trash2 />} label="삭제" tone="critical" onSelect={askDelete} />
</ResponsiveMenuGroup>
</ResponsiveMenuContent>
</ResponsiveMenu>설명 · 막힌 줄 — 데스크톱 표
표 줄의 ⋮ 는 "줄의 동작" 규칙 밖이다 — 그루 이음 관리 화면 차례에 다시 정할 때까지 Table 의 지금 문장대로 둔다.
이도윤 님은 휴가 내역이 없어 "휴가 내역 내보내기" 가 막혀 있어요.
import { Menu, MenuContent, MenuGroup, MenuItem, MenuTrigger } from "@/components/ui/menu"
<Menu>
<MenuTrigger asChild>
<Button variant="ghost" layout="iconOnly" aria-label="김하늘 더보기">
<EllipsisVertical />
</Button>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem label="수정" onSelect={edit} />
{/* 설명 — 이름만으로 무엇을 하는지 모를 줄에만 */}
<MenuItem label="비밀번호 초기화" description="새 비밀번호를 메일로 보내요." onSelect={reset} />
<MenuItem label="휴가 내역 내보내기" disabled={!hasVacations} onSelect={exportVacations} />
</MenuGroup>
<MenuGroup>
<MenuItem label="삭제" tone="critical" onSelect={askDelete} />
</MenuGroup>
</MenuContent>
</Menu>Behavior
| 인터랙션 | 동작 |
|---|---|
| 트리거 누르기 | 연다 · 열려 있으면 닫는다. 마우스로 열면 초점은 메뉴에 가고 링은 그리지 않는다 |
트리거에서 Enter · Space · ↓ | 열고 첫 줄로 — ↑ 면 마지막 줄로 |
↓ · ↑ | 다음 · 이전 줄 — 끝에서 처음으로 돈다. 막힌 줄은 건너뛴다 |
Home · End | 첫 줄 · 마지막 줄 |
| 글자 | 그 글자로 시작하는 줄로 — 한글은 첫 글자로 찾는다(입력기가 조합 중이면 키가 오지 않아 동작하지 않을 수 있다 — SEED 도 같다) |
줄 누르기 · Enter · Space | 메뉴를 닫고 초점을 트리거로 돌린 뒤 실행한다(닫히는 100ms 뒤) — 실행이 확인창 · 대화상자를 열어도 메뉴와 겹치지 않고, 그 창을 닫으면 초점이 ⋮ 로 돌아온다 |
Esc | 닫는다. 초점은 트리거로 |
Tab | 닫고 다음 요소로 간다 |
Shift+Tab | 닫고 트리거로 간다 |
| 바깥 누르기 | 닫는다 — 비모달이라 뒤 화면을 숨기지 않고 스크롤도 잠그지 않는다. 다른 칸 · 버튼을 눌렀으면 초점은 그곳에, 빈 자리를 눌렀으면 트리거로 |
| 마우스 호버 | 그 줄에 알약 바탕만 — 키보드 위치는 옮기지 않는다 |
| 막힌 줄 누르기 | 실행하지 않고 닫히지 않는다 |
| 실행이 확인을 부를 때 | 메뉴가 닫힌 뒤 Alert Dialog 를 연다 — 닫히는 메뉴와 열리는 확인창이 겹치지 않게(위 "줄 누르기" 의 순서) |
| 1280 아래로 줄면 | 열린 채 Menu Sheet 로 바뀐다(ResponsiveMenu). 묶음 이름은 시트에 자리가 없어 묶음 상자로만 나눈다 |
| 대화상자 · 시트 안에서 | 그 위(L3)에 뜬다. Esc · 바깥 누르기는 메뉴만 닫는다 |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 이름 fg-neutral 떠 있는 표면(bg-layer-floating) 위 16.41 · 다크 11.62, 설명 · 묶음 이름 fg-neutral-subtle 5.50 · 5.27, 위험 fg-critical 5.06 · 5.27 ✓. 막힌 줄은 예외(비활성) |
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 키보드 포커스 링 Desk 8.38 · 5.28 · HR 8.17 · 5.26 ✓ — 호버 알약(1.06)은 장식, 위치는 링이 알린다 |
| WCAG 2.1.1 Keyboard | 열기 · 옮기기 · 실행 · 닫기 모두 키보드로 된다 |
| WCAG 2.4.7 Focus visible | 키보드로 옮긴 줄에 늘 링 — 바탕색만으로 알리지 않는다 |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 줄 39 × 184 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 줄 39 ⚠ — 마우스용(1280 이상). 손가락으로 누르는 화면은 Menu Sheet(줄 52) ✓ |
| ARIA | 트리거 aria-haspopup="menu" · aria-expanded · aria-controls, 메뉴 role="menu" + 트리거로 aria-labelledby, 줄 role="menuitem"(막히면 aria-disabled="true"), 묶음 role="group"(이름이 있으면 aria-labelledby). 선은 장식이다. ⋮ 의 이름은 "{항목 · 화면 이름} 더보기"(표 줄은 "{줄 이름} 더보기") |
Do / Don't
✅ Do
- 목록 줄은 ⋮ 없이 누르면 들어가고, 동작은 들어간 화면에 — 하나(삭제)면 바닥 왼쪽 [삭제], 여럿이면 머리 ⋮ 하나.
- 위험한 동작은 맨 아래 묶음에
critical로. - 이름은 동사로 짧게 — 한 메뉴 안에서 말투를 맞춘다.
- 1280 미만에서는 같은 목록을 Menu Sheet 로.
❌ Don't
- 값을 고르는 데 메뉴를 쓰기 — 테마는 List 라디오 줄, 정렬 · 보기는 Segmented Control · Select.
- 좁은 목록 줄 끝마다 ⋮ — 줄에 ⋮ 를 둘 수 있는 건 콘텐츠가 높은 카드 머리뿐이다(표 줄은 Table).
- 수정 · 삭제 아이콘을 줄마다 늘 늘어놓기.
- 일부 줄에만 아이콘.
- 메뉴 안에 메뉴(하위 메뉴) · 단축키 표기 · 체크 줄.
- 이름 없는 ⋮.
Specification
menu.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Menu 를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
content | enabled | 최대 높이maxHeight | min(480px, 남은 화면)남은 화면이 200 보다 좁아도 200 은 둔다 — 먼저 줄이고, 200 아래로 내려가야 반대편으로 뒤집는다. 넘치면 메뉴 안 스크롤 |
| 너비width | 200px글이 길면 줄을 바꾼다 — 폭을 늘리지 않는다 | ||
| 상하 여백paddingY | spacing-x28px | ||
| 모서리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 — 화면 가장자리와도 8(Menu 는 1280 이상이라 안전 영역이 없다) | ||
| 정렬align | end트리거의 오른쪽 끝에 맞춘다(머리 ⋮ · 표 줄 끝 ⋮) — 왼쪽에 놓인 트리거는 start. 모자라면 화면 안으로 민다 | ||
groupLabel | enabled | 상하 여백paddingY | spacing-x28px |
| 좌우 여백paddingX | spacing-x416px | ||
| 글자·아이콘foreground | fg-neutral-subtle#62697A다크 #A2A8B7 | ||
| 글자typography | text-t313px / 18px | ||
| 굵기fontWeight | 400 | ||
divider | enabled | 높이height | 1px |
| 상하 바깥 여백marginY | spacing-x28px묶음 사이 8 + 선 1 + 8 | ||
| 좌우 바깥 여백marginX | spacing-x416px좌우 16 들인다 | ||
| 색color | stroke-neutral-subtle#EDEFF3다크 #353B4D | ||
item | enabled | 높이height | 39px한 줄 — 설명이 있으면 57. 글이 길면 줄을 바꿔 늘어난다(말줄임 없음) |
| 상하 여백paddingY | spacing-x2_510px | ||
| 좌우 여백paddingX | spacing-x416px | ||
| 간격gap | spacing-x28px | ||
| 커서cursor | pointer | ||
| 전환 대상transitionProperty | background-color, inset, border-radius알약 바탕 · 들임(0 → 8) · 모서리(0 → 12)가 함께 150ms 에 — SEED menu-item 은 들임 · 모서리 d3, 색 color-transition(둘 다 150ms) 이라 하나로 적었다(2026-10-15 사용자 결정 — 구현 세션 질문 Q-009-3, 안 B) | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
itemIcon | enabled | 크기size | 18pxlucide · 여러 줄이면 세로 가운데 |
| 색color | fg-neutral#1A1F2E다크 #F5F6FA | ||
itemLabel | enabled | 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA |
| 글자typography | text-t414px / 19px단어 단위 줄바꿈(v114) | ||
| 굵기fontWeight | 400 | ||
itemDescription | enabled | 간격gap | spacing-x0_52px이름 ↔ 설명 |
| 글자·아이콘foreground | fg-neutral-subtle#62697A다크 #A2A8B7 | ||
| 글자typography | text-t212px / 16px | ||
| 굵기fontWeight | 400 | ||
suffixIcon | enabled | 뒤 아이콘size | 16px |
| 뒤 아이콘 색color | fg-neutral#1A1F2E다크 #F5F6FA | ||
highlight | enabled | 바탕 들임insetX | 0px줄 폭 전체 — 마우스를 올리거나 누르면 8 로 들어온다(SEED menu-item enabled 에는 marginX 가 없다) |
| 모서리radius | 0px모서리 없음 — 들어오며 12 로 둥글어진다 | ||
| 배경background | transparent마우스를 올리거나 누를 때만 칠한다 | ||
highlight | hovered | 바탕 들임insetX | spacing-x28px좌우 8 들어온 알약 — 글은 알약 안쪽 8 지점에서 시작한다(SEED pressed marginX x2) |
| 모서리radius | radius-r312px12(SEED pressed cornerRadius r3) | ||
| 배경background | bg-layer-floating-pressed#F7F8FD다크 #353B4D메뉴 바탕(bg-layer-floating) 위의 누름 색 | ||
item | pressed | 배율scale | 콘텐츠 2px 거리 축소아이콘 · 글만 준다(SEED scaleScope: content) — 알약은 그대로. 모션 줄이기면 하지 않는다 |
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
highlight | pressed | 바탕 들임insetX | spacing-x28px |
| 모서리radius | radius-r312px | ||
| 배경background | bg-layer-floating-pressed#F7F8FD다크 #353B4D | ||
focusRing | focused | 링 두께width | 2px |
| 바탕 들임insetX | spacing-x28px알약이 다 들어온 자리 | ||
| 모서리radius | radius-r312px | ||
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | -2px알약 자리(좌우 8 들임 · 모서리 12) 안쪽에 — 바탕은 칠하지 않는다. 링은 그 자리에 고정이라 움직이지 않는다 — 들어오는 중인 호버 알약과 잠깐 폭이 다를 수 있다 | ||
item | disabled | 커서cursor | not-allowed |
itemIcon | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
itemLabel | disabled | 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997 |
itemDescription | disabled | 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997 |
suffixIcon | disabled | 뒤 아이콘 색color | fg-disabled#8A91A0다크 #838997 |
highlight | disabled | 배경background | transparent호버 · 누름 바탕이 없다 — 눌러도 실행하지 않고 닫히지 않는다 |
tone=critical
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
itemIcon | enabled | 색color | fg-critical#D72323다크 #FF8477 |
itemLabel | enabled | 글자·아이콘foreground | fg-critical#D72323다크 #FF8477설명은 fg-neutral-subtle 그대로 |
suffixIcon | enabled | 뒤 아이콘 색color | fg-critical#D72323다크 #FF8477 |
SEED 와 다른 점
- 크기는 small 하나 — SEED medium(240 · 줄 46)은 터치 화면용인데, groo 는 1280 미만에서 Menu Sheet 를 쓴다(시트 · 대화상자 결정 1).
- 호버 · 누름 바탕은 불투명한
bg-layer-floating-pressed· 선은stroke-neutral-subtle— SEED 의 투명도 있는 색(bg.transparent-pressed·stroke.neutral-muted)을 검사기가 받지 않는다(v102). Select 목록과 같다. - 위험 색은 groo
fg-critical— 떠 있는 표면 위 5.06 · 5.27 이라 SEED critical(라이트 3.76)의 대비 문제가 없다. - 설명 · 묶음 이름은 groo
fg-neutral-subtle— 5.50 · 5.27(SEED 3.42). - 줄은 단어 단위로 줄을 바꾼다(v114) — SEED 는 음절에서도 바꾼다.
- z-index 는 specs/z-index.md 의 L3(
z-floating200) — SEED 99999 는 Alert Dialog 위로 뜬다. - 1280 에서 Menu Sheet 와 바뀌는
ResponsiveMenu— SEED 는 둘을 따로 쓴다. - 트리거 끝에 맞춘다 — SEED 기본은 가운데(
bottom)라 표 줄 끝 · 머리 오른쪽 ⋮ 의 메뉴가 표 · 화면 밖으로 100 넘게 나간다. groo 는 오른쪽 끝(end)이 기본이다.
Migration notes
2026-10-25 — 좁은 목록 줄에서 ⋮ 를 걷는다
사용자가 당근 화면 다섯 장을 보고 정했다 — 당근은 ⋮ 를 콘텐츠가 높은 카드(구매 내역 카드 · 커뮤니티 글 — 사진 · 여러 줄 · 버튼)의 머리 옆에만 두고, 좁은 목록 줄(모임 목록)에는 두지 않는다. 좁은 줄은 한 단계 들어간 화면의 머리 ⋮ 에서 동작을 고른다. "디자인 시스템만 보고 우리 목록에 단순하게 붙인 문제" 라 "⋮ 는 목록 줄에서 빼고 대체 길을 준다 — 가계부든 카테고리 설정이든 뭐든". 2026-10-02 결정 중 "데스크톱 줄의 동작은 ⋮ + Menu" · "폰 스와이프는 지름길로 남기고 ⋮ 도" 의 ⋮ 를 목록 줄에서 걷었다 — 그 결정의 나머지(Menu small · 1280 미만 Menu Sheet · 메뉴는 실행만 · 비모달 키보드 …)는 그대로다.
- 규칙 — 목록 줄은 ⋮ 없이 누르면 들어간다. 들어간 화면의 동작이 하나(삭제)면 바닥 왼쪽 [삭제]
ghost+critical, 여럿이면 머리 오른쪽 ⋮ → Menu(1280 미만 Menu Sheet). 밀기는 폰 지름길로 남는다. 줄에 ⋮ 를 둘 수 있는 건 콘텐츠가 높은 카드 머리뿐이다. - 경계 — 데스크톱 데이터 표의 줄 끝 ⋮ 는 정하지 않았다(그루 이음 관리 화면 차례에). 2026-10-22 알림 목록 줄 ⋮(Popover 의 "알림 목록" — 알림 지우기 B)도 이 결정에서 다루지 않아 그대로 둔다.
- 담다 적용 — 가계부(폰 2026-10-24 걷음 · 데스크톱 07-2 도 없음) · 카테고리(줄 ⋮ 를 걷고 수정 시트 머리 ⋮ 에 하위 추가 · 거래 옮기기 · 삭제) · 할 일(⋮ 없음 · 상세 [수정][삭제]) · 할 일 태그(⋮ 없음 · 수정 시트 바닥 [삭제]). 이후 묶음(메모 · 계좌 · 반복 거래 · 프리셋 · 캘린더 관리 …)도 같은 규칙. 아래 2026-10-02 조사의 "→ ⋮ + Menu" · "→ 줄 끝 ⋮ + Menu Sheet" 는 이 규칙으로 읽는다(줄 누름 + 들어간 화면).
- 사이트 그림 — hero · 1280 그림 · 코드 미리보기를 메모 목록 줄 ⋮ 에서 메모 화면 머리 ⋮ 로, 플레이그라운드를 거래 줄 ⋮ 에서 거래 상세 머리 ⋮ 로 다시 그렸고, row-guide 는 맞음 둘(줄은 ⋮ 없이 · 들어간 화면 머리 ⋮) · 피함 둘(줄 끝 ⋮ · 늘 보이는 아이콘)이다. 코드 절 "줄의 ⋮" 를 "들어간 화면 머리 ⋮" 로(그림 id
ex-row→ex-header) — 레시피 예제(menu-examples)와 미리보기(build-preview-html)도 같게. 결정 글 — 디자인시스템 세션 decisions/migration-expense.md "줄 끝 ⋮ 원칙". 값 변경 없음.
2026-10-22 — 테마는 List 라디오 줄
"설정 값 고르기(테마 · 정렬) → Segmented Control" 이 Segmented Control 문서("폼 · 설정 값이 아니다")와 Select Box 이관 글(테마 = List 라디오 줄)과 어긋났다. 테마는 List 라디오 줄, 정렬 · 보기는 Segmented Control · Select 로 고치고 고르는 표는 Select 의 "고르는 컴포넌트 고르기" 로 링크한다(사용자 결정 1B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym). 사이트 그림 role-guide 의 왼쪽(설정 화면)을 List 라디오 줄로 다시 그렸다.
2026-10-15 — 알약이 들어오며 둥글어진다
구현 세션 질문 Q-009-3 — 2026-10-02 결정(1A "좌우 8 들인 알약")은 끝 모습만 정했고 움직임은 묻지 않았다. 사용자가 SEED 처럼 들임 0 → 8 · 모서리 0 → 12 를 150ms 에 들어오게 골랐다(안 B). item.transitionProperty 가 바탕 색만 적고 비고는 "들임 · 모서리가 함께" 라고 적어 한 줄 안에서 어긋나 있던 것도 고쳤다. 웹은 알약을 inset-x-0 · rounded-none 에서 호버 · 누름에 inset-x-x2 · rounded-r3 로 바꾸고 inset · border-radius 도 전환한다(지금은 8 고정 + 색만). 앱은 Menu 가 없다(1280 이상 전용 — Menu Sheet 는 줄 바탕이 묶음 폭 전체라 들임이 없다, SEED menu-sheet-item 도 같다).
2026-10-02 — SEED Menu 로 새로 둔다(옛 Dropdown Menu 를 대신)
사용자가 비교 페이지에서 여덟 가지를 모두 SEED 쪽으로 정했다 — Menu small · 1280 미만 Menu Sheet · 데스크톱 줄의 동작은 ⋮ + Menu · 폰 스와이프는 지름길로 남기고 ⋮ 도 · 메뉴는 실행만 · 비모달 키보드 · 쓰지 않는 능력 걷기 · 툴팁은 Help Bubble 모양의 보조. 옛 Dropdown Menu(160 · 줄 32 · 1px 테두리 · 모서리 8, 체크 · 라디오 · 단축키 · 하위 메뉴)는 걷었다 — 옛 스펙은 dropdown-menu.history/v-pre-seed-menu.*. 세 제품 모두 쓰는 곳이 없던 Context Menu · Menubar · Hover Card 도 걷었다(context-menu.history · menubar.history · hover-card.history).
제품은 앱 적용 단계에서 옮긴다(2026-10-02 조사 — Desk 웹 · HR 은 크로미움에 띄워 쟀고, Desk 앱은 위젯 테스트로 쟀다).
- Desk 웹 — 메뉴는 상단 바 테마 메뉴 하나(모달, 지금 값이 어디에도 안 보인다 → 걷고 설정의 Segmented Control 로). 데스크톱 관리 화면 11곳이 줄마다 수정 · 삭제 아이콘을 늘 보이고 그중 6개는 이름 · 툴팁이 없다(→ ⋮ + Menu). 메뉴 초점 표시가 바탕색뿐이라 1.12 · 1.15:1, 위험 항목 다크 3.00. 사이드바 아래 사용자 버튼(⇕)은 메뉴처럼 보이는데 눌러도 아무 일이 없다.
- Desk 앱 — PDropdownMenu 1곳(캘린더 공유 멤버 ⋮ — 이름 없음, 글자 15/600, 막힌 항목이 활성과 같은 색, 긴 이름 말줄임). 앱은 Menu 가 없으므로 이 ⋮ 를 Menu Sheet 로 옮기고 ⋮ 이름은 "{이름} 더보기",
PDropdownMenu는 지운다 — 화면 이관을 기다리지 않고 기초 PR 에서(2026-10-17 사용자 결정 — 03-F 결정 4A). - HR 웹 — 행 ⋮ 7곳(이름 0/7, 128 폭에서 "비밀번호 초기화" · 영어 이름이 두 줄로 접혀 줄이 32 ↔ 52), 초점 표시 1.06, 위험 3.76. 이동 메뉴 · 빵부스러기 메뉴는 모달, 행 ⋮ 는 비모달로 섞였다. 빵부스러기 … 는 이름이 "More Show more" 이고 16×16. 역할 목록 삭제 버튼은 늘 투명한데 눌린다.
- 앱 적용 때 화면마다 정할 자리 — Desk 앱 캘린더 공유 멤버 ⋮(Menu Sheet + 동사 이름 · 또는 웹처럼 권한 Select + 삭제 버튼), Desk 웹 사용자 버튼(⇕) · HR SpeedDial, HR 접힌 사이드바 · 빵부스러기(2026-10-04 · 10-08 정했다 — 접힌 사이드바의 하위는 1280 미만에서도 Side Navigation 의 펼침 메뉴(이 스펙의 1280 경계의 예외), 빵부스러기는 걷었다).
Menu Sheet
화면 아래에서 올라오는 동작 목록. 1280 미만에서 Menu 대신 뜬다 — 같은 줄 · 같은 순서. 들어간 화면 머리의 ⋮ 가 연다 — 목록 줄에는 ⋮ 를 두지 않는다(Menu 의 "줄의 동작"). 폰의 스와이프는 같은 동작의 지름길로 남는다. 입력 폼 · 상세 · 고르기는 Bottom Sheet, 되돌릴 수 없는 확인은 Alert Dialog 다.
Notification Badge
안 읽은 알림 · 확인하지 않은 새 것이 있다는 신호 — 점(있음)과 숫자(몇 개). 아이콘 버튼 · 탭 글에 붙고, 보면 사라진다. 대상의 상태 · 분류를 글로 보이는 것은 Badge 다.