Button
명확한 액션을 쉽게 실행하게 돕는 기본 인터랙션 컴포넌트. 폼 제출 · 대화상자 닫기 · 다음 단계처럼 사용자가 무엇을 하는지 분명한 동작에 쓴다.
구조는 당근 SEED Action Button(Apache-2.0)을 따른다 — 크기 4 · 배치 2 · 변형 7 · 상태 6. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-09-30 사용자 결정).
수치 원본은 button.yaml 이다. 아래 수치 표는 사이트가 그 파일로 그리고, GitHub 에서는 표 자리가 그 파일로 가는 링크로 보인다. 그림 자리([그림: …])도 같다 — 사이트는 그 자리에 스펙 값으로 그린 버튼과 화면 예시를 둔다.
직접 골라 보기
변형 · 크기 · 배치 · 상태를 고르면 스펙대로 그린 버튼과 그 코드가 바뀐다. 버튼은 실제로 눌러 볼 수 있다(호버 · 누름 · Tab 포커스).
import { Button } from "@/components/ui/button"
<Button>저장</Button>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Container | 배경 · 테두리 · 모양. 글자와 아이콘은 이 색(currentColor)을 따른다. |
| ⓑ Prefix Icon | 라벨 앞 아이콘 — 동작의 뜻을 돕는다. 선택. |
| ⓒ Label | 무엇을 하는지 동사로. 한 줄(white-space: nowrap). |
| ⓓ Suffix Icon | 라벨 뒤 아이콘 — chevron 처럼 동작을 돕는다. 선택. |
| ⓔ Focus ring | 키보드 포커스에만 2px 링 · 2px 띄움(v106). |
부위
| 부위 | 설명 |
|---|---|
root | 버튼 상자(Container) — 배경 · 테두리 · 모양, 글자 · 아이콘 색(currentColor) |
label | 글자(Label) |
prefixIcon | 앞 아이콘(Prefix Icon) — 동작의 뜻을 돕는다 |
suffixIcon | 뒤 아이콘(Suffix Icon) — chevron 처럼 동작을 돕는다 |
icon | 아이콘만 있는 버튼의 아이콘 |
progressCircle | 로딩 원 |
focusRing | 키보드 포커스 링(웹 · 앱 — 키보드로 옮길 때만) |
Properties
Size
네 가지다. 크기는 이름이 아니라 높이로 고른다.
small(36) ·medium(40) — 화면 안에서 두루 쓴다. 모달 footer 는small.large(48) — CTA. 모바일 하단 고정 버튼 · 모바일 모달 footer.xsmall(32) — 좁은 자리에서 쓰는 알약 모양.
누르는 영역은 보이는 크기와 따로 가로 · 세로 44 까지 넓힌다(v106 — 44 는 반드시).
크기
| size | 높이 | 모서리 | 로딩 원 | 로딩 원 두께 | 터치 (AA · AAA) | 설명 |
|---|---|---|---|---|---|---|
xsmall | 32px — 최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) | radius-full 9999px | 14px | 2px | ✓ · ✓ | 32 — 좁은 자리, 알약 모양 |
small | 36px — 최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) | radius-r2 8px | 14px | 2px | ✓ · ✓ | 36 — 화면 안 범용, 모달 footer |
medium (기본) | 40px — 최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) | radius-r2 8px | 16px | 2px | ✓ · ✓ | 40 — 화면 안 범용, 아이콘만 있는 버튼의 기본 |
large | 48px — SEED 52 — groo 는 48(사용자 결정). 최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) | radius-r3 12px | 18px | 2px | ✓ · ✓ | 48 — CTA(모바일 하단 · 모바일 모달 footer) |
글자와 함께 쓸 때(withText):
크기별 여백 · 글자 · 아이콘 — 글자
| 속성 | xsmall | small | medium (기본) | large |
|---|---|---|---|---|
| 상하 여백 | spacing-x1_5 6px | spacing-x2 8px | spacing-x2_5 10px | spacing-x3 12px — (48 − 줄 높이 24) ÷ 2 — SEED 52 는 14 |
| 좌우 여백 | spacing-x3_5 14px | spacing-x3_5 14px | spacing-x4 16px | spacing-x5 20px |
| 간격 | spacing-x1 4px | spacing-x1 4px | spacing-x1_5 6px | spacing-x2 8px |
| 글자 | text-t3 13px / 18px | text-t4 14px / 19px | text-t4 14px / 19px | text-t6 18px / 24px |
| 앞 아이콘 | 14px | 14px | 16px | 22px |
| 뒤 아이콘 | 14px | 14px | 16px | 22px |
아이콘만 쓸 때(iconOnly):
크기별 여백 · 아이콘 — 아이콘만
| 속성 | xsmall | small | medium (기본) | large |
|---|---|---|---|---|
| 너비 | 32px | 36px | 40px | 48px |
| 여백 | spacing-x1_5 6px | spacing-x2 8px | spacing-x2_5 10px | spacing-x3 12px |
| 아이콘 | 14px | 16px | 18px | 22px |
모든 조합에 공통인 값:
공통
| 부위 | 상태 | 속성 | 값 | 비고 |
|---|---|---|---|---|
root | enabled | 커서 | pointer | |
root | enabled | 터치 영역 | 44 × 44 | 보이는 크기와 따로 — 누르는 영역을 가로 · 세로 44 까지 넓힌다(v106). AA ✓ · AAA ✓ |
root | enabled | 전환 대상 | background-color, color | |
root | enabled | 전환 시간 | motion-duration-color-transition 150ms | |
root | enabled | 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | |
root | enabled | 누름 | 누른 동작이 끝날 때까지 한 번 | onClick 이 Promise 를 돌려주면 이행 · 거부될 때까지 다음 누름(포인터 · Enter · Space)을 버린다 — 시간 창은 없다. 비동기 동작은 Promise 를 돌려준다(mutateAsync). 돌려주지 않으면 잠그지 않는다. 앱은 Future 가 끝날 때까지. 사용자 결정 2026-10-08 |
label | enabled | 글꼴 | font-sans | |
label | enabled | 굵기 | 700 | |
focusRing | focused | 링 두께 | 2px | 웹 · 앱 같다 — 앱은 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다. 앱 버튼도 포커스를 받아 Tab 순서에 든다(사용자 결정 2026-10-08) |
focusRing | focused | 링 간격 | 2px | |
focusRing | focused | 링 색 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA | |
root | pressed | 배율 | 세로 2px 축소 | 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px — Feedback 의 눌림 피드백(v104) |
root | pressed | 축소 시간 | motion-duration-pressed-scale 150ms | |
root | pressed | 축소 곡선 | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | |
root | loading | 커서 | progress | |
root | loading | 누름 | 막음 | 누르기(포인터 · Enter · Space)를 삼켜 두 번 제출을 막는다 — 축소도 없다. aria-busy=true. SEED 는 막지 않는다 |
label | loading | 색 | transparent | 글자 · 아이콘을 투명하게 해 폭을 그대로 둔다 |
root | disabled | 커서 | not-allowed | 포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) |
root | disabled | 배경 | color-bg-disabled #F5F6FA · 다크 #353B4D | |
root | disabled | 글자·아이콘 | color-fg-disabled #8A91A0 · 다크 #838997 |
Layout
라벨과 아이콘의 조합이다 — 글자만 · 앞 아이콘 + 글자 · 글자 + 뒤 아이콘 · 아이콘만.
- 앞 · 뒤 아이콘을 함께 쓰지 않는다. 앞은 동작의 뜻을 돕고(추가의
+), 뒤는 동작을 돕는다(다음의 chevron). - 아이콘은 꼭 필요할 때만 쓴다 — 라벨을 읽기 어려워진다.
- 아이콘만은 아이콘만으로 뜻을 전해 접근성이 떨어진다. 꼭 필요할 때만 쓰고, 이름(
aria-label또는 옆 Tooltip)을 반드시 단다. 정사각이고, 아이콘 버튼의 보이는 크기 기본은medium(40 — v106). - 상단 바의 아이콘 버튼은 이 Button 이 아니다 — Top Navigation 의 아이콘 버튼(상자 44 = 누르는 영역 · 아이콘 24 · 버튼끼리 붙음)이다(2026-10-04). 본문 안의 아이콘 버튼은 그대로
medium·iconOnly(40 · 18)다.
Variant
일곱 가지다. 화면에서 강조하려는 정도에 따라 고른다.
| 변형 | 생김새 | 쓰는 곳 |
|---|---|---|
brandSolid | 브랜드 색 채움 | 서비스의 핵심 액션 하나 — Desk "거래 추가", HR "휴가 신청". 한 화면에 하나 |
neutralSolid | 짙은 회색 채움 | 대부분의 CTA — 저장 · 확인 · 다음. 한 화면에 하나 |
criticalSolid | 빨강 채움 | 삭제 · 초기화처럼 되돌릴 수 없는 작업의 확정. 주로 Alert Dialog |
neutralWeak | 옅은 회색 채움 | CTA 를 뺀 대부분의 액션. CTA 옆에서는 보조(취소 · 닫기) |
brandOutline | 테두리 + 브랜드 글자 | Solid 보다 낮은 위계. Solid 와 함께 쓰지 않고 neutralOutline 과 짝 |
neutralOutline | 테두리 + 본문 글자 | 가장 낮은 위계의 보조 액션. Solid 와 함께 쓰지 않고 brandOutline 과 짝 |
ghost | 배경 없음 | 메뉴 · 툴바 · 목록 행의 가벼운 액션. 글자색을 바꿀 수 있다(아래) |
변형별 색
| 속성 | brandSolid | neutralSolid (기본) | neutralWeak | criticalSolid | brandOutline | neutralOutline | ghost |
|---|---|---|---|---|---|---|---|
| 배경 | color-bg-brand-solid HR #075B3C · Desk #0147AD | color-bg-neutral-inverted #1A1F2E · 다크 #F5F6FA | color-bg-neutral-weak #F5F6FA · 다크 #353B4D | color-bg-critical-solid #D72323 · 다크 #CC0E17 | transparent | transparent | transparent |
| 글자·아이콘 | color-static-white #FFFFFF | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-static-white #FFFFFF | color-fg-brand HR #075B3C · Desk #0147AD | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA |
| 테두리 | — | — | — | — | color-stroke-neutral-weak #E5E8EF · 다크 #404757 — SEED stroke.neutral-muted(투명도 있는 검정)의 불투명 짝 | color-stroke-neutral-weak #E5E8EF · 다크 #404757 | — |
| 테두리 두께 | — | — | — | — | 1px | 1px | — |
| 로딩 원 트랙 | color-static-white #FFFFFF × 30% | color-fg-neutral-inverted #FFFFFF × 30% · 다크 #242938 × 30% | color-gray-500 #8A91A0 · 다크 #656B78 | color-static-white #FFFFFF × 30% | color-bg-brand-weak-pressed HR #D6E8DE · Desk #D7E5FC | color-gray-500 #8A91A0 · 다크 #656B78 | color-gray-500 #8A91A0 · 다크 #656B78 |
| 로딩 원 채움 | color-static-white #FFFFFF | color-fg-neutral-inverted #FFFFFF · 다크 #242938 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-static-white #FFFFFF | color-stroke-brand-solid HR #075B3C · Desk #0147AD — Progress Circle brand 톤과 같다 — 다크는 밝은 짝(바탕 위 라이트 Desk 8.38 · HR 8.17, 다크 Desk 6.10 · HR 6.07 — 트랙과 Desk 6.59 · 4.73). 2026-10-08 까지 bg-brand-solid 라 다크 바탕 위 1.73 이었다(채움 색은 다크에서도 짙다) | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA |
ghost 는 글자색을 바꿀 수 있다(SEED ghost 의 color) — 배경 · 누름은 그대로다.
ghostColor | 쓰는 곳 |
|---|---|
neutral(기본) | 대부분의 ghost |
neutralSubtle | 목록 행 · 툴바의 보조 아이콘 액션, 가장자리 텍스트 버튼(flush) |
brand | 강조하는 가벼운 액션 — 본문 속 "자세히 보기" |
critical | 확인 창을 여는 삭제 — 모달 footer · 시트 바닥 왼쪽. 확정은 확인 창의 criticalSolid |
ghost 글자색
| 조합 | 상태 | 속성 | 값 |
|---|---|---|---|
variant=ghost + ghostColor=neutralSubtle | enabled | 글자·아이콘 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 |
variant=ghost + ghostColor=brand | enabled | 글자·아이콘 | color-fg-brand HR #075B3C · Desk #0147AD |
variant=ghost + ghostColor=critical | enabled | 글자·아이콘 | color-fg-critical #D72323 · 다크 #FF8477 |
size=xsmall + layout=withText | enabled | 좌우 여백 | spacing-x3_5 14px |
size=xsmall + layout=withText | enabled | 상하 여백 | spacing-x1_5 6px |
size=xsmall + layout=withText | enabled | 간격 | spacing-x1 4px |
size=xsmall + layout=withText | enabled | 글자 | text-t3 13px / 18px |
size=xsmall + layout=withText | enabled | 앞 아이콘 | 14px |
size=xsmall + layout=withText | enabled | 뒤 아이콘 | 14px |
size=xsmall + layout=iconOnly | enabled | 너비 | 32px |
size=xsmall + layout=iconOnly | enabled | 여백 | spacing-x1_5 6px |
size=xsmall + layout=iconOnly | enabled | 아이콘 | 14px |
size=small + layout=withText | enabled | 좌우 여백 | spacing-x3_5 14px |
size=small + layout=withText | enabled | 상하 여백 | spacing-x2 8px |
size=small + layout=withText | enabled | 간격 | spacing-x1 4px |
size=small + layout=withText | enabled | 글자 | text-t4 14px / 19px |
size=small + layout=withText | enabled | 앞 아이콘 | 14px |
size=small + layout=withText | enabled | 뒤 아이콘 | 14px |
size=small + layout=iconOnly | enabled | 너비 | 36px |
size=small + layout=iconOnly | enabled | 여백 | spacing-x2 8px |
size=small + layout=iconOnly | enabled | 아이콘 | 16px |
size=medium + layout=withText | enabled | 좌우 여백 | spacing-x4 16px |
size=medium + layout=withText | enabled | 상하 여백 | spacing-x2_5 10px |
size=medium + layout=withText | enabled | 간격 | spacing-x1_5 6px |
size=medium + layout=withText | enabled | 글자 | text-t4 14px / 19px |
size=medium + layout=withText | enabled | 앞 아이콘 | 16px |
size=medium + layout=withText | enabled | 뒤 아이콘 | 16px |
size=medium + layout=iconOnly | enabled | 너비 | 40px |
size=medium + layout=iconOnly | enabled | 여백 | spacing-x2_5 10px |
size=medium + layout=iconOnly | enabled | 아이콘 | 18px |
size=large + layout=withText | enabled | 좌우 여백 | spacing-x5 20px |
size=large + layout=withText | enabled | 상하 여백 | spacing-x3 12px — (48 − 줄 높이 24) ÷ 2 — SEED 52 는 14 |
size=large + layout=withText | enabled | 간격 | spacing-x2 8px |
size=large + layout=withText | enabled | 글자 | text-t6 18px / 24px |
size=large + layout=withText | enabled | 앞 아이콘 | 22px |
size=large + layout=withText | enabled | 뒤 아이콘 | 22px |
size=large + layout=iconOnly | enabled | 너비 | 48px |
size=large + layout=iconOnly | enabled | 여백 | spacing-x3 12px |
size=large + layout=iconOnly | enabled | 아이콘 | 22px |
State
| 상태 | 모습 |
|---|---|
enabled | 기본 |
hovered | 웹. 누름 색과 같다(v106), 축소는 없다 |
focused | 웹 · 앱. 키보드 포커스에만 링 2px · 띄움 2px(v106). 앱은 키보드로 옮길 때만 그리고 손가락으로 누를 때는 없다(2026-10-08) |
pressed | 누름 색 + 세로 2px 거리 축소(v104 — 기초 Feedback 의 눌림 피드백) |
loading | 누름 색 위 로딩 원. 라벨 자리는 그대로(폭 유지) |
disabled | 전용 색(bg-disabled · fg-disabled, v106). 불투명도로 흐리게 하지 않는다 |
| 기본 enabled | 호버 hovered | 포커스 focused | 누름 pressed | 로딩 loading | 비활성 disabled | |
|---|---|---|---|---|---|---|
| brandSolid | ||||||
| neutralSolid | ||||||
| neutralWeak | ||||||
| criticalSolid | ||||||
| brandOutline | ||||||
| neutralOutline | ||||||
| ghost |
변형별 상태 변화
| variant | hovered 웹 — 누름 색과 같다(v106), 축소는 없다 | pressed 누름 색 + 세로 2px 거리 축소(v104) | loading 누름 색 위 로딩 원, 라벨 자리 그대로 · 누름을 자동으로 막는다 | disabled 전용 색(v106) — 불투명도로 흐리게 하지 않는다 |
|---|---|---|---|---|
brandSolid | 배경 color-bg-brand-solid-pressed HR #064F33 · Desk #013D96 | 배경 color-bg-brand-solid-pressed HR #064F33 · Desk #013D96 | 배경 color-bg-brand-solid-pressed HR #064F33 · Desk #013D96 | — |
neutralSolid | 배경 color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC | 배경 color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC | 배경 color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC | — |
neutralWeak | 배경 color-bg-neutral-weak-pressed #EDEFF3 · 다크 #404757 | 배경 color-bg-neutral-weak-pressed #EDEFF3 · 다크 #404757 | 배경 color-bg-neutral-weak-pressed #EDEFF3 · 다크 #404757 | — |
criticalSolid | 배경 color-bg-critical-solid-pressed #C01016 · 다크 #D82424 | 배경 color-bg-critical-solid-pressed #C01016 · 다크 #D82424 | 배경 color-bg-critical-solid-pressed #C01016 · 다크 #D82424 | — |
brandOutline | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — SEED bg.transparent-pressed 의 불투명 짝 | — | 배경 transparent · 테두리 color-stroke-neutral-weak #E5E8EF · 다크 #404757 |
neutralOutline | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | — | 배경 transparent · 테두리 color-stroke-neutral-weak #E5E8EF · 다크 #404757 |
ghost | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | 배경 color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | 배경 transparent |
로딩
loading이면 로딩 원(크기는 Size 표)을 라벨 자리 가운데 둔다. 라벨은 글자 · 아이콘 색만 투명하게 해서 버튼 폭이 그대로다 — 버튼이 자식을 따로 감싸지 않으므로 부르는 쪽의[&>span]·[&>svg]규칙이 그대로 먹는다.- 로딩 중에는 누르기를 자동으로 막는다(두 번 제출 방지) — 포인터 · Enter · Space 누르기를 삼키고(
onClick을 부르지 않는다), 누름 축소도 없다. 포커스는 그대로 두고aria-busy="true"를 단다. SEED 는 로딩이 비활성을 포함하지 않지만 groo 는 막는다 — 사용자 결정. - 로딩 원 색은 변형마다 다르다(Solid 는 흰 원, Brand Outline 은 옅은 브랜드 트랙 위 브랜드 선 색
stroke-brand-solid— Progress Circle brand 톤과 같고 다크에서는 밝은 짝, 나머지는 회색 트랙 위 본문색).
Width
- 내용 맞춤(기본) — 라벨 길이만큼.
- 채움 — 컨테이너 폭을 채운다(
w-full). 모바일 하단 고정 CTA · 폼의 마지막 제출. - 최소 너비는 두지 않는다. 둘을 나란히 채울 때는 비율로 나눈다(아래 배치).
Guidelines
Hierarchy
시각적 주목도는 배경 대비로 정해진다. 화면에서 강조하려는 정도에 따라 변형을 고른다.
가장 중요한 CTA
대부분의 액션, 강과 짝
중요도가 낮은 보조 액션
| 강조 | 변형 | 화면 안 개수 | 쓰는 곳 |
|---|---|---|---|
| 강 — 대비가 강한 배경 | brandSolid · neutralSolid · criticalSolid | 1개 | 가장 중요한 CTA |
| 중 — 대비가 약한 배경 | neutralWeak | 여러 개 | 대부분의 액션, 강 버튼과 짝 |
| 약 — 투명한 배경 | brandOutline · neutralOutline · ghost | 여러 개 | 중요도가 낮은 보조 액션 |
상황에 따라 알맞은 변형 쓰기
화면 안의 중요도에 따라 고른다. 같은 저장이라도 그 화면의 CTA 면 neutralSolid, 서비스의 중심 동작이면 brandSolid 다.
브랜드 색은 꼭 필요한 곳에만
브랜드 색은 로고 · 대표 버튼 · 핵심 메시지 같은 브랜드 상징에 모아 쓴다. 많이 쓰면 뜻과 강조가 흩어진다.
brandSolid는 서비스의 중심 동작 하나에만 — Desk "거래 추가", HR "휴가 신청"처럼. 어떤 화면이 여기에 드는지는 앱 적용 때 화면마다 정한다.- 저장 · 확인 · 다음 같은 대부분의 CTA 는
neutralSolid다.
예외 — 증권 매매 단추
증권 종목 상세의 [매도] · [매수] 한 쌍만 국내 통념을 따른다 — [매도] brandSolid(파랑) · [매수] criticalSolid(빨강), large 한 줄에 반씩 · 사이 8. 국내 증권 앱 사용자가 매수 빨강 · 매도 파랑을 이미 익혀 있어 그 기대를 지킨다. 이 자리에서 criticalSolid 는 "되돌릴 수 없는 확정" 이 아니고 brandSolid 는 "서비스의 중심 동작" 이 아니다 — 이 한 쌍 밖으로 넓히지 않는다(2026-10-29 사용자 결정 C7 B — https://claude.ai/artifact/QLQiAkvfez7bqpc4DEuiCH). 거절: 매수 neutralSolid · 매도 neutralWeak(스펙 그대로).
버튼 조합
- Solid 조합 —
neutralWeak+neutralSolid(또는brandSolid). 위계가 분명하고 부담이 적다. - Outline 조합 —
neutralOutline+brandOutline. 강조가 낮은 보조 액션을 한 화면에 여러 번 둘 때. - Outline 은 Solid 와 함께 쓰지 않는다.
버튼 배치
- 닫기 · 초기화처럼 Dismiss 뜻의
neutralWeak와 CTA 를 나란히 채울 때는 3:7 로 나눈다 — 위계가 분명해진다. 모달 footer 는 Dialog · Bottom Sheet 의 폭 나누기(지금은 균등)를 따른다. - 비슷한 위계의
neutralWeak둘은 나란히 둘 수 있다. - 셋 이상 나란히 두는 것은 피한다(SEED "세 개 이상 나란히 사용하는 것은 권장하지 않습니다") — 중요도가 비슷해 보여 고르기 어렵고, 큰 글자에서 라벨이 잘린다. 더 있으면 아이콘만 버튼(더보기)으로 넘긴다.
- 인접한 버튼 사이는 8(
spacing-x2).
놓는 바탕 — neutralWeak 의 채움과 비활성 채움(bg-disabled)은 라이트에서 회색 바닥(bg-layer-basement)과 같은 색(gray-200)이다(v108). 흰 표면(카드 · 흰 띠 · 흰 화면 · 시트 · 모달 — bg-layer-default · bg-layer-floating) 위에 두고, 회색 바닥에 바로 둘 땐 neutralOutline 을 쓴다 — 바닥 위의 neutralWeak 는 채움이 사라져 글자만 남는다(1.00:1). 데스크톱 화면 제목 줄의 버튼이 그 자리다. 기초 규칙은 DESIGN.md Elevation 의 "바닥 위의 채움"(2026-10-16 사용자 결정 — 기초 웹 01-040 B)이다.
모달 footer(groo)
- 오른쪽에
[취소 neutralWeak] [저장 neutralSolid]— 웹은small(36), 모바일 전체 폭은large(48). - 확인 창을 여는 삭제는 왼쪽에
ghost+critical글자. 삭제의 확정은 Alert Dialog 의criticalSolid. - 1280 미만 Bottom Sheet 바닥도 같은 꼴이다 — [삭제] 는 왼쪽
ghost+critical(large· 글자만큼의 폭), 주 버튼([저장] · [수정])이 남은 폭을 채운다. - 목록 줄을 눌러 들어간 화면(상세 · 수정)에서 동작이 삭제 하나뿐이면 이 바닥 왼쪽 [삭제] 로 둔다 — 머리 ⋮ 는 동작이 여럿일 때다(Menu 의 "줄의 동작", 2026-10-25 사용자 결정).
- 모달 footer 의 버튼을
neutralOutline둘로 두지 않는다 — 전체 폭 버튼 둘이 테두리로 서면 위계가 흐려진다.
라벨
기초 Writing(v106)을 따른다 — 사용자가 할 행동을 동사로, 해요체 없이 짧게, 마침표 없이.
- "다음" 보다 "저장" · "주문 확인"처럼 무엇을 하는지 알게.
- 같은 동작에는 같은 말을 쓴다.
긴 라벨
긴 라벨 · 번역 · 큰 글자로 나란한 두 버튼이 넘치면 세로로 쌓는다(주 버튼이 위). 한 줄에 억지로 줄이지 않는다.
짝 버튼은 SEED ResponsivePair 를 따른다 — 어느 한 버튼의 라벨이 짝 폭의 절반을 넘으면 둘 다 세로로 쌓고, 라벨은 한 줄을 지킨다(버튼 안에서 줄을 바꾸지 않는다). 대가 — 글자 크기 200% 에서는 가장 긴 라벨이 한 줄을 지키느라 버튼 밖으로 넘칠 수 있다(약 170). 2026-10-17 사용자 결정 — 03A-013 A.
큰 글자에서 높이 — 크기마다의 높이(32 · 36 · 40 · 48)는 최소 높이다. 글자가 커져 라벨이 상자보다 높아지면 상자가 글을 따라 커지고 글을 자르지 않는다(앱 200% 에서 36 · 40 · 48 → 54 · 58 · 72, 100% 는 그대로). 웹은 높이가 rem 이라 이미 그렇다. SEED 디자인 원본(action-button root.minHeight)과 같고, 칩(03-E 결정 1 C)과 같은 규칙이다. 대가 — 같은 200% 에서 웹(72 · 80 · 96)이 앱보다 크다. 2026-10-22 사용자 결정 — 버튼 큰 글자 A(고르지 않은 안: 글 -static · 지금처럼 잘림 · 글 1.5배 상한).
이 판정은 부품 하나가 맡는다 — 웹 ResponsivePair · 앱 PResponsivePair(자식 둘 — 보조 버튼 · 주 버튼 순서, 쌓이면 주 버튼이 위). 시트 · 확인창 바닥의 짝 버튼도 이 부품을 쓴다 — 같은 판정을 바닥마다 따로 적지 않는다(2026-10-21 사용자 결정 — I-01 결과 확인 결정 6A).
아이콘
아이콘은 버튼의 동작을 눈으로 보이게 돕는다. 앞(Prefix)은 액션의 뜻을, 뒤(Suffix)는 chevron 처럼 동작을 돕는다. 라벨을 읽기 어려워지지 않게 꼭 필요할 때만 쓰고, 앞 · 뒤를 함께 쓰지 않는다.
로딩
저장처럼 시간이 걸리는 액션은 누른 버튼을 로딩으로 바꾼다. 라벨을 바꾸거나 비활성으로 돌리지 않는다.
Button 과 Chip
SEED 는 Button 과 Chip 을 이렇게 가른다. groo 도 같다 — 선택 · 필터는 Chip(같은 내용을 2 ~ 4가지로 바로 다르게 보면 Segmented Control)이 맡는다. Toggle Group 은 2026-10-09 걷었다.
| Button | Chip | |
|---|---|---|
| 목적 | 액션 실행 | 정보 표현 + 선택 |
| 예 | 완료 · 제출 · 다음 · 삭제 | 필터(서초4동 외 34) · 옵션(0 ~ 6개월) |
| 라벨 | 보면 동작이 예상된다 | 지금 켜진 조건 · 정보 |
| 쓰는 모양 | 하나로도 | 둘 이상 묶어서 |
groo 에만 있는 것
반반 바(split bar) — 액션 둘이 무게가 같고 한 묶음으로 읽힐 때, 본문 폭을 채운 네모 바를 반으로 갈라 각 칸에 하나씩 둔다(예: 내역 분할의 항목 추가 · 균등 분할).
- 컨테이너:
display:flex; width:100%; background:var(--color-bg-layer-basement); border:1px solid var(--color-stroke-neutral-weak); border-radius:var(--radius-r2); overflow:hidden. - 각 칸:
flex:1의ghost버튼, 모서리 없음(컨테이너가 깎는다). - 칸 사이는 1px
stroke-neutral-weak구분선을 글자 높이만큼만 긋는다 — 끝까지 그으면 두 칸이 벽으로 막힌 것처럼 보인다. - 얇게 —
xsmall높이(32). 목록에 줄을 더하는 성격이라 본문 행보다 무거우면 안 된다. 알약 모양은 쓰지 않는다(segmented 와 헷갈린다). - 선택이 아니라 실행이다. 눌린 상태(
data-state=on)가 없다. 셋 이상으로 나누지 않는다.
가장자리 맞춤(flush) — SEED 의 bleed 와 같은 기능이다.
flush="left" | "right"— 그 방향 가로 여백을 0 으로 한다.- 대상:
ghost+ 앞 아이콘이 컨테이너 가장자리의 첫 · 끝 요소일 때. 글자만 있는 ghost 에는 쓰지 않는다. - flush ghost 는 텍스트 버튼이다 — 누름 · 호버에 배경을 깔지 않고 글자색으로만 반응한다(
neutralSubtle→ 누르면fg-neutral). 한쪽 여백만 0 이라 채움 상자가 좌우 비대칭이 되기 때문이다. - 그래서 flush 는
ghostColor를 덮는다 — 빨간 글자를 지켜야 하는 삭제(critical)에는 쓰지 않는다. - 아이콘만 있는 단추(
iconOnly)는 여백을 0 으로 하지 않고 상자째 번진다 — 줄의 첫 · 끝에 선 아이콘 단추(달 줄의 ‹ › · 줄 끝 닫기)는 상자를 바깥으로 (상자 − 아이콘) ÷ 2 만큼 내어(음수 여백) 아이콘이 화면 여백 선에 닿게 한다 — small 36 · 아이콘 16 이면 10, 앱 40 · 아이콘 18 이면 11. 상자가 대칭으로 남으므로 누름 · 호버 바탕은 보통 단추처럼 깐다. 누르는 영역 44 도 같이 번진다(줄 안이면 List 의 "줄 끝 작은 버튼" 처럼 줄보다 이긴다). 아이콘이 여백 선 안쪽으로 10 남짓 들어가 글 · 목록 줄과 왼쪽 · 오른쪽 선이 어긋나던 것을 막는다(2026-10-29 I-29 — I-08 7 달 줄 ← −10 · 앱 가계부 머리_bleedEnd선례).
코드
레시피 recipes/shadcn/components/ui/button.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.
기본
import { Button } from "@/components/ui/button"
<Button>저장</Button>변형
<Button variant="brandSolid">거래 추가</Button>
<Button variant="neutralSolid">저장</Button>
<Button variant="neutralWeak">취소</Button>
<Button variant="criticalSolid">삭제</Button>
<Button variant="brandOutline">자세히</Button>
<Button variant="neutralOutline">건너뛰기</Button>
<Button variant="ghost">더보기</Button>크기
<Button size="xsmall">라벨</Button>
<Button size="small">라벨</Button>
<Button size="medium">라벨</Button>
<Button size="large">라벨</Button>아이콘
import { ChevronRight, Plus, Search } from "lucide-react"
<Button variant="brandSolid"><Plus />거래 추가</Button>
<Button variant="neutralWeak">전체 보기<ChevronRight /></Button>
<Button variant="ghost" ghostColor="neutralSubtle" layout="iconOnly" aria-label="검색">
<Search />
</Button>ghost 글자색
<Button variant="ghost">편집</Button>
<Button variant="ghost" ghostColor="neutralSubtle">더보기</Button>
<Button variant="ghost" ghostColor="brand">자세히 보기</Button>
<Button variant="ghost" ghostColor="critical">삭제</Button>비활성 · 로딩
const [saving, setSaving] = useState(false)
<Button disabled>저장</Button>
<Button loading={saving} onClick={async () => { setSaving(true); await save(); setSaving(false) }}>
저장
</Button>채움
<Button size="large" className="w-full">저장</Button>모달 footer
<footer className="flex items-center gap-2">
<Button variant="ghost" ghostColor="critical" size="small" className="mr-auto">삭제</Button>
<Button variant="neutralWeak" size="small">취소</Button>
<Button size="small">저장</Button>
</footer>Behavior
| 인터랙션 | 동작 |
|---|---|
| Click / Tap | onClick 발화. disabled · loading 이면 무시. |
| 누른 동작이 끝나기 전 | 다시 눌러도 무시 — onClick 이 Promise 를 돌려주면 이행 · 거부될 때까지 다음 누름(포인터 · Enter · Space)을 버린다. 시간 창은 없다. 비동기 동작은 Promise 를 돌려준다(mutateAsync) — 돌려주지 않으면 잠그지 않는다. 앱은 Future 가 끝날 때까지(사용자 결정 2026-10-08) |
Keyboard Enter · Space | 클릭과 같다(포커스 상태에서). |
Keyboard Tab | 다음 포커스로. Shift+Tab 은 거꾸로. |
asChild | <Slot> 으로 요소를 바꾼다(예: <a>). loading 과 함께 쓰지 않는다(Slot 은 자식 하나만 받는다). |
| Disabled | 누르기 · 키보드 불가, 포커스에서 빠진다. |
Form 안 — type="submit" 을 적어야 제출한다. type="button" 은 제출하지 않는다.
앱 — 버튼(PButton)도 포커스를 받아 Tab 순서에 든다. 하드웨어 키보드로 옮길 때만 웹과 같은 링(2px · 띄움 2px · stroke-focus-ring)을 그리고 — Flutter 의 FocusHighlightMode.traditional — 손가락으로 누를 때는 그리지 않는다(사용자 결정 2026-10-08, DESIGN.md State › 앱의 키보드 포커스).
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 글자 대비(4.5:1) | 흰 글자 × bg-brand-solid · bg-critical-solid, fg-neutral-inverted × bg-neutral-inverted(누름 포함), fg-neutral × bg-neutral-weak — npm run lint:all 의 role 짝이 잰다 |
| WCAG 1.4.3 비활성 | fg-disabled — 비활성 컴포넌트 글자는 대비 예외(1.4.3 incidental) |
| WCAG 1.4.11 UI 대비(3:1) | 포커스 링 stroke-focus-ring × 표면. Brand Outline 의 로딩 원 stroke-brand-solid × 표면 — 라이트 Desk 8.38 · HR 8.17, 다크 Desk 6.10 · HR 6.07 |
| WCAG 2.5.8 Target Size — Minimum(AA, 24×24) | 네 크기 모두 통과 |
| WCAG 2.5.5 Target Size — Enhanced(AAA, 44×44) | 보이는 크기는 large 48 만 넘지만, 누르는 영역을 44 까지 넓혀 모두 통과(v106) |
| WCAG 2.4.7 Focus visible | 키보드 포커스에만 링(focus-visible) — 앱도 키보드로 옮길 때만 같은 링(2026-10-08) |
| ARIA | <button> 기본. 아이콘만이면 aria-label 필수. 로딩이면 aria-busy="true". asChild 로 <a> 를 쓸 때 role="button" 을 따로 달지 않는다 |
| 모션 줄이기 | 축소를 하지 않는다(v104 모드) — 누름은 색으로만 |
Do / Don't
✅ Do
- 강조 버튼(
brandSolid·neutralSolid·criticalSolid)은 한 화면에 하나. - 모달 footer 는
[취소 neutralWeak] [저장 neutralSolid], 삭제는 왼쪽ghost+critical. - 되돌릴 수 없는 작업의 확정은 확인 창의
criticalSolid. - 증권 매매 한 쌍만 예외 — [매도]
brandSolid· [매수]criticalSolid(국내 통념). - 아이콘만 있는 버튼에는 이름을 단다.
- 모바일 하단 CTA 는
large+ 채움 + 안전 영역(pb-safe).
❌ Don't
- 강조 버튼을 한 화면에 여럿.
- 셋 이상을 한 줄에 — 피한다, 더 있으면 더보기.
brandSolid를 일반 저장 · 확인에 — 브랜드 색이 흩어진다.- Outline 을 Solid 와 섞기.
- 앞 · 뒤 아이콘을 함께.
disabled버튼에 호버 · 누름 반응.
Specification
button.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 버튼을 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 커서cursor | pointer |
| 터치 영역touchTarget | 44 × 44보이는 크기와 따로 — 누르는 영역을 가로 · 세로 44 까지 넓힌다(v106). AA ✓ · AAA ✓ | ||
| 전환 대상transitionProperty | background-color, color | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
| 누름press | 누른 동작이 끝날 때까지 한 번onClick 이 Promise 를 돌려주면 이행 · 거부될 때까지 다음 누름(포인터 · Enter · Space)을 버린다 — 시간 창은 없다. 비동기 동작은 Promise 를 돌려준다(mutateAsync). 돌려주지 않으면 잠그지 않는다. 앱은 Future 가 끝날 때까지. 사용자 결정 2026-10-08 | ||
label | enabled | 굵기fontWeight | 700 |
| 글꼴fontFamily | font-sansPretendard, Inter, sans-serif | ||
focusRing | focused | 링 두께width | 2px웹 · 앱 같다 — 앱은 키보드로 옮길 때만 그린다(Flutter FocusHighlightMode.traditional), 손가락으로 누를 때는 없다. 앱 버튼도 포커스를 받아 Tab 순서에 든다(사용자 결정 2026-10-08) |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | 2px | ||
root | pressed | 배율scale | 세로 2px 축소기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px — Feedback 의 눌림 피드백(v104) |
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
root | loading | 커서cursor | progress |
| 누름press | 막음누르기(포인터 · Enter · Space)를 삼켜 두 번 제출을 막는다 — 축소도 없다. aria-busy=true. SEED 는 막지 않는다 | ||
label | loading | 색color | transparent글자 · 아이콘을 투명하게 해 폭을 그대로 둔다 |
root | disabled | 배경background | bg-disabled#F5F6FA다크 #353B4D |
| 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997 | ||
| 커서cursor | not-allowed포인터 이벤트는 켜 둔다 — 끄면 이 커서가 안 보인다(SEED 와 같다) |
variant=brandSolid
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | bg-brand-solidDesk#0147AD다크 #1049A4HR#075B3C다크 #1B583E |
| 글자·아이콘foreground | static-white#FFFFFF | ||
progressCircle | enabled | 로딩 원 트랙track | static-white / 30%rgba(255, 255, 255, 0.3) |
| 로딩 원 채움range | static-white#FFFFFF | ||
root | hovered | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
root | pressed | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
root | loading | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
variant=neutralSolid
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | bg-neutral-inverted#1A1F2E다크 #F5F6FA |
| 글자·아이콘foreground | fg-neutral-inverted#FFFFFF다크 #242938 | ||
progressCircle | enabled | 로딩 원 트랙track | fg-neutral-inverted / 30%rgba(255, 255, 255, 0.3)다크 rgba(36, 41, 56, 0.3) |
| 로딩 원 채움range | fg-neutral-inverted#FFFFFF다크 #242938 | ||
root | hovered | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
root | pressed | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
root | loading | 배경background | bg-neutral-inverted-pressed#535866다크 #B7BDCC |
variant=neutralWeak
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | bg-neutral-weak#F5F6FA다크 #353B4D |
| 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA | ||
progressCircle | enabled | 로딩 원 트랙track | gray-500#8A91A0다크 #656B78 |
| 로딩 원 채움range | fg-neutral#1A1F2E다크 #F5F6FA | ||
root | hovered | 배경background | bg-neutral-weak-pressed#EDEFF3다크 #404757 |
root | pressed | 배경background | bg-neutral-weak-pressed#EDEFF3다크 #404757 |
root | loading | 배경background | bg-neutral-weak-pressed#EDEFF3다크 #404757 |
variant=criticalSolid
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | bg-critical-solid#D72323다크 #CC0E17 |
| 글자·아이콘foreground | static-white#FFFFFF | ||
progressCircle | enabled | 로딩 원 트랙track | static-white / 30%rgba(255, 255, 255, 0.3) |
| 로딩 원 채움range | static-white#FFFFFF | ||
root | hovered | 배경background | bg-critical-solid-pressed#C01016다크 #D82424 |
root | pressed | 배경background | bg-critical-solid-pressed#C01016다크 #D82424 |
root | loading | 배경background | bg-critical-solid-pressed#C01016다크 #D82424 |
variant=brandOutline
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | transparent |
| 글자·아이콘foreground | fg-brandDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757SEED stroke.neutral-muted(투명도 있는 검정)의 불투명 짝 | ||
| 테두리 두께borderWidth | 1px | ||
progressCircle | enabled | 로딩 원 트랙track | bg-brand-weak-pressedDesk#D7E5FC다크 #1F3A69HR#D6E8DE다크 #214132 |
| 로딩 원 채움range | stroke-brand-solidDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84Progress Circle brand 톤과 같다 — 다크는 밝은 짝(바탕 위 라이트 Desk 8.38 · HR 8.17, 다크 Desk 6.10 · HR 6.07 — 트랙과 Desk 6.59 · 4.73). 2026-10-08 까지 bg-brand-solid 라 다크 바탕 위 1.73 이었다(채움 색은 다크에서도 짙다) | ||
root | hovered | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
root | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4DSEED bg.transparent-pressed 의 불투명 짝 |
root | disabled | 배경background | transparent |
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757 |
variant=neutralOutline
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | transparent |
| 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA | ||
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757 | ||
| 테두리 두께borderWidth | 1px | ||
progressCircle | enabled | 로딩 원 트랙track | gray-500#8A91A0다크 #656B78 |
| 로딩 원 채움range | fg-neutral#1A1F2E다크 #F5F6FA | ||
root | hovered | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
root | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
root | disabled | 배경background | transparent |
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757 |
variant=ghost
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 배경background | transparent |
| 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA | ||
progressCircle | enabled | 로딩 원 트랙track | gray-500#8A91A0다크 #656B78 |
| 로딩 원 채움range | fg-neutral#1A1F2E다크 #F5F6FA | ||
root | hovered | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
root | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
root | loading | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
root | disabled | 배경background | transparent |
variant=ghost, ghostColor=neutralSubtle
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 글자·아이콘foreground | fg-neutral-subtle#62697A다크 #A2A8B7 |
variant=ghost, ghostColor=brand
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 글자·아이콘foreground | fg-brandDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 |
variant=ghost, ghostColor=critical
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 글자·아이콘foreground | fg-critical#D72323다크 #FF8477 |
size=xsmall
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 32px최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) |
| 모서리radius | radius-full9999px(알약) | ||
progressCircle | enabled | 로딩 원size | 14px |
| 로딩 원 두께thickness | 2px |
size=small
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 36px최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) |
| 모서리radius | radius-r28px | ||
progressCircle | enabled | 로딩 원size | 14px |
| 로딩 원 두께thickness | 2px |
size=medium
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 40px최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) |
| 모서리radius | radius-r28px | ||
progressCircle | enabled | 로딩 원size | 16px |
| 로딩 원 두께thickness | 2px |
size=large
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 48pxSEED 52 — groo 는 48(사용자 결정). 최소 높이 — 글이 상자보다 크면(큰 글자) 상자가 글을 따라 늘어난다(2026-10-22 사용자 결정 — 버튼 큰 글자 A, SEED action-button root.minHeight) |
| 모서리radius | radius-r312px | ||
progressCircle | enabled | 로딩 원size | 18px |
| 로딩 원 두께thickness | 2px |
size=xsmall, layout=withText
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 상하 여백paddingY | spacing-x1_56px |
| 좌우 여백paddingX | spacing-x3_514px | ||
| 간격gap | spacing-x14px | ||
label | enabled | 글자typography | text-t313px / 18px |
prefixIcon | enabled | 앞 아이콘size | 14px |
suffixIcon | enabled | 뒤 아이콘size | 14px |
size=xsmall, layout=iconOnly
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 너비width | 32px |
| 여백padding | spacing-x1_56px | ||
icon | enabled | 아이콘size | 14px |
size=small, layout=withText
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 상하 여백paddingY | spacing-x28px |
| 좌우 여백paddingX | spacing-x3_514px | ||
| 간격gap | spacing-x14px | ||
label | enabled | 글자typography | text-t414px / 19px |
prefixIcon | enabled | 앞 아이콘size | 14px |
suffixIcon | enabled | 뒤 아이콘size | 14px |
size=small, layout=iconOnly
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 너비width | 36px |
| 여백padding | spacing-x28px | ||
icon | enabled | 아이콘size | 16px |
size=medium, layout=withText
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 상하 여백paddingY | spacing-x2_510px |
| 좌우 여백paddingX | spacing-x416px | ||
| 간격gap | spacing-x1_56px | ||
label | enabled | 글자typography | text-t414px / 19px |
prefixIcon | enabled | 앞 아이콘size | 16px |
suffixIcon | enabled | 뒤 아이콘size | 16px |
size=medium, layout=iconOnly
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 너비width | 40px |
| 여백padding | spacing-x2_510px | ||
icon | enabled | 아이콘size | 18px |
size=large, layout=withText
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 상하 여백paddingY | spacing-x312px(48 − 줄 높이 24) ÷ 2 — SEED 52 는 14 |
| 좌우 여백paddingX | spacing-x520px | ||
| 간격gap | spacing-x28px | ||
label | enabled | 글자typography | text-t618px / 24px |
prefixIcon | enabled | 앞 아이콘size | 22px |
suffixIcon | enabled | 뒤 아이콘size | 22px |
size=large, layout=iconOnly
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 너비width | 48px |
| 여백padding | spacing-x312px | ||
icon | enabled | 아이콘size | 22px |
SEED 와 다른 점
large는 48 이다(SEED 52) — 여백 · 글자 · 아이콘은 SEED large 그대로다.- 테두리 · 투명 누름은 SEED 가 투명도 있는 색(
stroke.neutral-muted·bg.transparent-pressed)이라, 불투명한 가장 가까운 역할(stroke-neutral-weak·bg-layer-default-pressed)을 쓴다 — design.md 검사기가 8자리 hex 를 받지 않는다. - 웹의
hovered·focused상태를 더한다(v106).focused는 앱의 키보드 포커스에도 그린다(2026-10-08). - 누름은 누른 동작이 끝날 때까지 한 번이다(
onClick이 돌려준 Promise) — SEED 는 정하지 않았다. - 로딩 중 누르기를 자동으로 막는다(SEED 는 막지 않는다).
neutralSolid의 누름은 새 역할bg-neutral-inverted-pressed(gray-800, v112)다.
Migration notes
2026-10-30 — 아이콘 단추의 가장자리 맞춤
"가장자리 맞춤(flush)" 에 아이콘만 있는 단추의 규칙을 더했다 — 여백을 0 으로 하는 글 단추와 달리 상자째 (상자 − 아이콘) ÷ 2 만큼 번져 아이콘이 화면 여백 선에 닿는다(2026-10-29 I-29 — I-08 7 달 줄 · 앱 _bleedEnd 선례). 값 · 토큰 변경 없음 — 번지는 양은 크기 표(상자 · 아이콘)에서 나온다. 레시피 flush 는 아직 글 단추용(여백 0)이고, 아이콘 단추는 놓는 자리가 음수 여백으로 번진다. 옛 스펙은 button.history/v-pre-cleanup-1030.md.
2026-10-29 — 증권 매매 단추 예외
사용자 결정 C7 B — 지시서가 "지금 뜻 그대로" 로 넘긴 [매도] brandSolid · [매수] criticalSolid 가 변형 뜻(브랜드 = 중심 동작 하나 · 빨강 = 되돌릴 수 없는 확정)과 맞지 않아 그림으로 물었다. 국내 통념을 이 한 쌍의 예외로 적는다. 앱 매도는 옛 bg-informative-solid → brandSolid, 사이 10 → 8. 값 · 토큰 변경 없음.
2026-10-25 — 시트 바닥의 [삭제] 도 모달 footer 꼴
사용자 결정 — 목록 줄에는 ⋮ 를 두지 않고, 들어간 화면의 동작이 하나(삭제)면 바닥 왼쪽 [삭제] ghost + critical 이다(button.md 모달 footer 꼴 — 시트도 같게, Menu 의 "줄의 동작"). "모달 footer" 에 시트 바닥 · 들어간 화면 두 줄을 더하고, critical 의 쓰임에 시트 바닥을 적었다. 시트 바닥의 폭(삭제 글자만큼 · 주 버튼이 남은 폭)은 모달 footer 의 왼쪽 삭제를 시트의 전체 폭 버튼에 옮긴 것이다. 값 변경 없음.
2026-10-22 — 셋 이상 나란히는 "피한다"
"셋 이상 나란히 두지 않는다" 를 SEED 말 그대로 "피한다" 로 낮췄다(남은 규칙 R7 — 화면은 같다). "강조 버튼은 한 화면에 하나"(SEED Hierarchy 표 "화면 내 개수 1개") · "모달 footer 를 neutralOutline 둘로 두지 않는다"(SEED 조합 규칙과 같은 쪽)는 그대로다(R8 · R9). 사용자 결정 — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L. 값 변경 없음.
2026-10-08 — 앱 적용이 남긴 것
앱 적용(desk-front #428 ~ #430 · desk-app #407 ~ #410)이 남긴 문제를 사용자가 비교 페이지에서 정했다.
- Brand Outline 의 로딩 원(8) — 호가
bg-brand-solid라 다크의 투명한 버튼 위에서 1.73:1(트랙과 1.34)이었다. Progress Circle brand 톤과 같은stroke-brand-solid(다크는 밝은 짝 — 6.10)로 바꿨다. 트랙bg-brand-weak-pressed는 그대로다. - 두 번 누름(F3) —
onClick이 Promise 를 돌려주면 끝날 때까지 다음 누름을 버린다. 시간 창(Desk 웹 Button 의 옛 600ms)은 두지 않는다 — 앱 옛 시트의 잠금과 같다. 로딩 중에 누름을 삼키는 것은 그대로다. 레시피의onClick은 돌려준 값을 받는 꼴((event) => unknown)이 됐다. - 앱의 키보드 포커스(E2) — 키보드로 옮길 때만 웹과 같은 링을 그리고,
PButton도 포커스를 받는다(DESIGN.md State · Inclusive Design).
2026-09-30 — SEED Action Button 구조로
사용자가 비교 페이지(https://claude.ai/artifact/JRfDTChty6VDZzzqpWSF6C)에서 정했다 — 변형은 SEED 7가지만(옛 dangerSoft 는 없앰, 정말 위험한 건 criticalSolid) · 일반 CTA 는 neutralSolid · 크기는 SEED 틀 + large 48 · 모양은 SEED(모서리 8 · 12 · 알약, 글자 700) · 확인 창을 여는 삭제는 ghost + critical · brandSolid 는 핵심 액션에만 · 로딩은 SEED 모양 + 누름 자동 막기.
제품(Desk 웹 197 · 앱 133 · HR 웹 176곳)은 앱 적용 단계에서 옮긴다. 그동안 옛 이름은 아래처럼 읽는다.
| 옛 변형 | 새 변형 |
|---|---|
default(정보 파랑 채움) | neutralSolid(일반 CTA) · brandSolid(핵심 액션) |
destructive | criticalSolid |
secondary | neutralWeak |
outline | neutralOutline |
dangerSoft | ghost + critical(확인 창을 여는 삭제) |
ghost | ghost — 보조 아이콘 액션은 neutralSubtle |
accent · link | ghost + brand |
| 옛 크기 | 새 크기 |
|---|---|
sm(32) | xsmall — 알약 |
default(36) | small |
md(40) | medium |
lg(48) | large |
icon(40 — Desk 웹은 36, button-variants.ts:60) | medium · iconOnly. 상단 바는 Top Navigation 의 아이콘 버튼 |
iconLg(36 원형, 모바일 헤더) | 상단 바는 Top Navigation 의 아이콘 버튼(44 · 24 — 2026-10-04), 그 밖은 medium · iconOnly(보이는 크기 40 — v106) |
그 밖에 바뀐 것 — 누름은 밝기 95% · 0.98 배 → 누름 색 + 2px 거리 축소(v104), 비활성은 50% 불투명 → 전용 색(v106), 글자 굵기 500 → 700, 모서리 4 → 8(large 12, xsmall 알약), 아이콘–글자 간격 8 고정 → 크기마다(4 · 4 · 6 · 8), 로딩 스피너는 라벨 앞 → 라벨 자리.
앱에 옮길 때 — cn 부터. 새 cva 는 groo 스케일 이름(text-t4 · px-x4 · rounded-r2)을 쓴다. 기본 tailwind-merge 는 text-t4 를 글자색으로 읽어 앞의 text-fg-neutral-inverted 를 지우고(짙은 버튼의 흰 글자가 사라진다), px-x4 를 여백으로 못 읽어 p-0 덮어쓰기가 먹지 않는다. 레시피 lib/utils.ts 처럼 spacing · radius · text 스케일을 extendTailwindMerge 에 등록한다 — Desk 웹의 cn 은 지금 옛 글자 이름만 안다.
다른 컴포넌트에 남은 옛 이름 — 버튼을 부르는 레시피 코드(alert-dialog · calendar · carousel · pagination · sidebar)와 그 스펙, 모달 footer 규칙(dialog · drawer · alert-dialog)은 이 변경에서 옮겼다. 아래는 옛 이름을 글로만 적고 있어 각 컴포넌트 차례에 옮긴다 — 예제 속 JSX(tooltip · sheet · popover · form · dropdown-menu · drawer · dialog · card · alert-dialog · table · spinner 예제), 스펙 본문(table · popover · spinner · context-menu · dropdown-menu · calendar 의 nav outline — toggle-group 은 2026-10-09 걷었고 icon-picker 는 Input Button 으로, collapsible 은 2026-10-10 Accordion 항목 모양의 트리거로 다시 썼다), DESIGN.md 의 다른 컴포넌트 절(Form 의 button-primary lg · Pagination 의 페이지 버튼), 미리보기의 빈 화면 · 폼 · 결재 행(Outline 과 Solid 를 한 줄에 둔다 — Outline 조합 규칙과 어긋난다).
2026-09-16 — default(36) 를 정식 사이즈로 올리고 모달 footer 를 그걸로
Sizes 표는 원래 sm 32 · md 40 · lg 48 셋이었는데, desk 웹 구현에는 shadcn 이름을 그대로
둔 default(높이 36 · 좌우 양쪽 16 · 14px)가 하나 더 있었고 그게 cva 기본값이다. 모달 footer 20종 실측에서
표준 footer 40 과 손수 footer 36 이 섞여 있었고, 화면으로 보니 40 은 대화상자에 비해 굵어 사용자가 36 을 골랐다 —
2026-09-30 구조에서는 small(36)이 그 자리다.
2026-08 — 채움 버튼을 info 로, 모달 footer 좌측을 옅은 채움으로
default 채움색을 브랜드 남색에서 정보 파랑으로 바꾸고(남색은 버튼 채움으로 무겁다), 모달 footer 의 취소는 secondary, 삭제는 dangerSoft 로 두었다. 2026-09-30 에 일반 CTA 는 neutralSolid, 취소는 neutralWeak, 삭제는 ghost + critical 로 다시 정했다.
Bottom Sheet
화면 아래에서 올라오는 모달. 1280 미만에서 폼 · 상세 · 고르기를 띄운다 — 같은 내용을 1280 이상에서는 Dialog 로 띄운다(한 부품이 폭으로 바꾼다 — Responsive Dialog). 되돌릴 수 없는 확인은 Alert Dialog, 들어간 화면 머리 ⋮ 의 동작 목록은 Menu Sheet, 화면 높이의 90% 를 넘는 내용은 페이지다.
Callout
화면 안, 그 기능 · 내용 가까이에 늘 보이는 안내 상자 — 팁 · 제약 · 주의, 그리고 그 자리에서 난 오류(저장 실패). 페이지 전체의 상태는 Page Banner, 잠깐 알릴 결과는 Snackbar, 눌러서 여는 도움말은 Help Bubble 이다.