Select
짧은 선택지 5개 이상에서 폼에 넣을 값을 고르는 칸. 결제 수단 · 통화 · 휴가 정책 · 반복 단위처럼 한 줄 글(+ 한 줄 설명)로 충분한 선택지를 칸 아래 목록으로 연다. 달력 · 시각 · 아이콘 격자 · 검색이 필요한 긴 목록은 Input Button, 설명 · 그림이 붙는 2 ~ 6개는 Select Box다.
구조는 당근 SEED Select(Apache-2.0)를 따른다 — 트리거(Trigger) · 목록(Content) · 선택지(Item) · 묶음(Group) · 묶음 사이 선(Divider). 라벨 · 설명 · 오류는 Field가 둘레에서 그린다. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-01 사용자 결정).
수치 원본은 select.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
크기 · 상태 · 여럿 고르기 · 앞 아이콘 · 묶음 · 설명을 고르면 스펙대로 그린 칸과 그 코드가 바뀐다. 실제로 열고 고를 수 있다.
import { Field } from "@/components/ui/field"
import { Select, SelectGroup, SelectItem } from "@/components/ui/select"
<Field label="결제 수단">
<Select placeholder="결제 수단 선택" value={asset} onValueChange={setAsset}>
<SelectGroup>
<SelectItem value="none" label="결제 수단 없음" />
</SelectGroup>
<SelectGroup label="카드">
<SelectItem value="kb-check" label="국민 체크카드" />
<SelectItem value="hyundai-m" label="현대카드 M" />
<SelectItem value="shinhan" label="신한카드 Deep" />
</SelectGroup>
<SelectGroup label="계좌 · 현금">
<SelectItem value="toss" label="토스뱅크 통장" />
<SelectItem value="kakao" label="카카오뱅크 통장" />
<SelectItem value="cash" label="현금" />
</SelectGroup>
</Select>
</Field>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Trigger | 트리거 — 고른 값이나 placeholder 를 보이고, 누르면 목록을 연다. 상자는 Input Button · Text Input 과 같다. |
| ⓑ Prefix Icon | 트리거 앞 아이콘 — 없어도 된다. 하나를 고르면 그 선택지의 아이콘으로 바뀐다. |
| ⓒ Value · Placeholder | 고른 값 — 한 줄. 고르기 전에는 고를 값의 종류("결제 수단 선택"). |
| ⓓ Chevron | 셰브론 — 목록이 열려 있는지 알린다. 열리면 180° 돈다. |
| ⓔ Content | 목록 — 트리거 폭 그대로 트리거 아래에 붙는다(모자라면 위). |
| ⓕ Group Label | 묶음 제목 — 없어도 된다. |
| ⓖ Item | 선택지 — 앞 아이콘 · 글 · 설명(한 줄). 고른 선택지는 오른쪽에 체크(Indicator). |
| ⓗ Divider | 묶음 사이 선 — 묶음이 둘 이상이면 저절로 그린다. 선택지 사이에는 없다. |
부위
| 부위 | 설명 |
|---|---|
trigger | 트리거 — Input Button 과 같은 상자. 누르면 목록이 열린다 |
value | 고른 값 — 한 줄. 여럿이면 쉼표로 잇고, 넘치면 "첫 값 외 N개" |
placeholder | 고르기 전의 글 — "{값의 종류} 선택"("결제 수단 선택"). 라벨만 그대로 두지 않는다 |
prefixIcon | 트리거 앞 아이콘 — 하나를 고르면 그 선택지의 아이콘으로 바뀐다 |
chevron | 셰브론 — 열리면 180° 돈다 |
focusRing | 트리거의 키보드 포커스 링(웹) |
content | 목록 — 칸 폭 그대로, 칸 아래(모자라면 위) 8 |
item | 선택지 한 줄 — 글은 목록 안에서 줄바꿈된다 |
itemIcon | 선택지 앞 아이콘 |
itemLabel | 선택지 글 — 명사형으로 짧게 |
itemDescription | 선택지 설명 — 한 줄. 트리거에는 보이지 않는다 |
indicator | 고른 표시 — 오른쪽 체크. 바탕 · 굵기는 바꾸지 않는다 |
highlight | 누름 · 호버 · 키보드 위치 — 좌우 8 들인 알약 |
groupLabel | 묶음 제목 |
divider | 묶음 사이 선 — 선택지 사이에는 없다 |
Properties
Size
large(트리거 52 · 선택지 46)는 폰 · 앱에서, medium(트리거 40 · 선택지 39)은 1280 이상 데스크톱 웹(마우스)에서만 쓴다. 웹의 기본은 responsive 다 — 1280 미만은 large, 이상은 medium(SEED lg). 앱은 늘 large 다. 예외 하나 — Table Pagination 의 두 고르기는 줄 한 높이를 지키려고 폭과 상관없이 medium 40 이다(사용자 결정 2026-10-08). 트리거와 목록은 같은 크기를 쓰고, 한 폼 안에서 크기를 섞지 않는다.
크기 — 트리거
| size | trigger 최소 높이 | trigger 좌우 여백 | trigger 간격 | trigger 모서리 | trigger 바뀌는 폭 | 값 글자 | placeholder 글자 | 앞 아이콘 | chevron 크기 | 설명 |
|---|---|---|---|---|---|---|---|---|---|---|
large | 52px | spacing-x4 16px | spacing-x2_5 10px | radius-r3 12px | — | text-t5 16px / 22px | text-t5 16px / 22px | 20px | 20px | 트리거 52 · 선택지 46 — 폰 · 앱 |
medium | 40px | spacing-x3_5 14px | spacing-x2 8px | radius-r2 8px | — | text-t4 14px / 19px | text-t4 14px / 19px | 16px | 16px | 트리거 40 · 선택지 39 — 1280 이상 데스크톱 웹(마우스)에서만 |
responsive (기본) | — | — | — | — | 1280px — 미만은 large, 이상은 medium 의 값을 쓴다(SEED lg) — 앱은 늘 large | — | — | — | — | 폭에 따라 — 1280 미만 large · 이상 medium. 웹의 기본 |
크기 — 선택지
| size | item 높이 | item 상하 여백 | item 간격 | itemIcon 크기 | itemLabel 글자 | itemDescription 글자 | indicator 크기 | 설명 |
|---|---|---|---|---|---|---|---|---|
large | 46px — 한 줄 — 설명이 있으면 66 | spacing-x3 12px | spacing-x3 12px | 22px | text-t5 16px / 22px | text-t3 13px / 400 / 18px | 14px | 트리거 52 · 선택지 46 — 폰 · 앱 |
medium | 39px — 한 줄 — 설명이 있으면 57 | spacing-x2_5 10px | spacing-x2 8px | 18px | text-t4 14px / 19px | text-t2 12px / 400 / 16px | 12px | 트리거 40 · 선택지 39 — 1280 이상 데스크톱 웹(마우스)에서만 |
responsive (기본) | — | — | — | — | — | — | — | 폭에 따라 — 1280 미만 large · 이상 medium. 웹의 기본 |
크기 — 묶음 제목
| size | groupLabel 상하 여백 | groupLabel 글자 | groupLabel 굵기 | 설명 |
|---|---|---|---|---|
large | spacing-x2_5 10px | text-t4 14px / 400 / 19px | 500 | 트리거 52 · 선택지 46 — 폰 · 앱 |
medium | spacing-x2 8px | text-t3 13px / 400 / 18px | 400 | 트리거 40 · 선택지 39 — 1280 이상 데스크톱 웹(마우스)에서만 |
responsive (기본) | — | — | — | 폭에 따라 — 1280 미만 large · 이상 medium. 웹의 기본 |
State
트리거의 상태는 Input Button 과 같고, 목록이 열려 있는 동안 셰브론이 180° 돈다.
| 트리거 | 모습 |
|---|---|
enabled | 투명 바탕 · 안쪽 1px stroke-neutral-weak |
pressed | 바탕 bg-layer-default-pressed + 값 · 아이콘만 2px 거리로 준다(테두리 · 바탕은 그대로). 마우스는 호버에 같은 바탕 |
focused | 키보드 포커스에만 바깥 링 2px · 띄움 2px stroke-focus-ring — 트리거는 버튼이라 마우스 · 터치로 눌러서는 링이 없다 |
open | 셰브론 180°(열 때 150ms · 닫을 때 100ms) |
invalid | 안쪽 2px stroke-critical-solid — 눌러도 그대로. 오류 글은 Field 가 칸 아래에 |
disabled | 바탕 bg-disabled · 글자 · 아이콘 fg-disabled. 흐리게 하지 않는다(v106) |
readonly | 바탕 bg-disabled · 값은 진한 글자 그대로. 포커스는 되고 열리지 않는다 |
트리거 상태
| 상태 | trigger 배경 | trigger 테두리 | trigger 테두리 두께 | trigger 배율 | trigger 커서 | trigger 축소 시간 | trigger 축소 곡선 |
|---|---|---|---|---|---|---|---|
enabled 기본 | transparent | color-stroke-neutral-weak #E5E8EF · 다크 #404757 | 1px — 상자 안쪽(inset). 오류의 2px 는 안쪽에 덧그린다 | — | pointer | — | — |
pressed 누름 — 트리거는 바탕 + 콘텐츠 축소, 선택지는 알약 + 콘텐츠 축소. 마우스 호버 · 선택지의 키보드 위치는 같은 바탕 · 알약(축소 없음) | color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — SEED bg.transparent-pressed 의 불투명 짝. 마우스는 호버에 같은 바탕 | color-stroke-neutral-weak #E5E8EF · 다크 #404757 | 1px — 상자 안쪽(inset). 오류의 2px 는 안쪽에 덧그린다 | 콘텐츠 2px 거리 축소 — 값 · 아이콘만 준다(SEED scaleScope: content) — 테두리 · 바탕은 그대로 | pointer | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) |
invalid 오류 — 트리거 안쪽 2px. Field 의 invalid | transparent | color-stroke-critical-solid #D72323 · 다크 #FF8477 — 1px 위에 2px 가 나타난다(100ms) | 2px | — | pointer | — | — |
disabled 비활성 — 트리거 · 선택지 | color-bg-disabled #F5F6FA · 다크 #353B4D — 흐리게 하지 않는다(v106) | color-stroke-neutral-weak #E5E8EF · 다크 #404757 | 1px — 상자 안쪽(inset). 오류의 2px 는 안쪽에 덧그린다 | — | not-allowed | — | — |
readonly 읽기 전용 — 트리거, 열리지 않는다 | color-bg-disabled #F5F6FA · 다크 #353B4D — 값은 진한 글자 그대로 — 열리지 않는다 | color-stroke-neutral-weak #E5E8EF · 다크 #404757 | 1px — 상자 안쪽(inset). 오류의 2px 는 안쪽에 덧그린다 | — | default | — | — |
트리거 글자 · 아이콘
| 상태 | 값 글자색 | placeholder 색 | 앞 아이콘 색 | chevron 색 | chevron 회전 |
|---|---|---|---|---|---|
enabled 기본 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-placeholder #767C8B · 다크 #838997 | color-fg-neutral-muted #535866 · 다크 #B7BDCC | color-fg-neutral-muted #535866 · 다크 #B7BDCC | — |
open 목록이 열림 — 셰브론 180° | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-placeholder #767C8B · 다크 #838997 | color-fg-neutral-muted #535866 · 다크 #B7BDCC | color-fg-neutral-muted #535866 · 다크 #B7BDCC | 180deg |
disabled 비활성 — 트리거 · 선택지 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | — |
포커스 링
| 상태 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|
enabled 기본 | — | — | — |
focused 웹 — 트리거에 키보드 포커스 링 | 2px | 2px | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA — 트리거 — 키보드 포커스에만(마우스 · 터치로 눌러서는 없다) |
| 선택지 | 모습 |
|---|---|
enabled | 바탕 없음 |
pressed | 좌우 8 들어온 알약(모서리 12) bg-layer-floating-pressed + 콘텐츠 2px 거리 축소. 마우스 호버 · 키보드로 옮긴 선택지는 같은 알약(축소 없음) |
selected | 오른쪽 체크(large 14 · medium 12). 바탕 · 글 굵기는 바꾸지 않는다 |
disabled | 글 · 설명 · 아이콘 · 체크 모두 fg-disabled. 알약이 생기지 않는다 |
선택지 상태
| 상태 | item 배율 | item 커서 |
|---|---|---|
enabled 기본 | — | pointer |
pressed 누름 — 트리거는 바탕 + 콘텐츠 축소, 선택지는 알약 + 콘텐츠 축소. 마우스 호버 · 선택지의 키보드 위치는 같은 바탕 · 알약(축소 없음) | 콘텐츠 2px 거리 축소 — 누르는 동안만(호버 · 키보드 위치는 축소 없음) | pointer |
disabled 비활성 — 트리거 · 선택지 | — | not-allowed |
알약 — 모양
| 속성 | 값 | 비고 |
|---|---|---|
| 배경 | transparent | 누를 때 · 키보드로 옮길 때만 칠한다 |
| 모서리 | radius-r3 12px |
알약
| 상태 | highlight 바탕 들임 | highlight 배경 |
|---|---|---|
enabled 기본 | — | transparent — 누를 때 · 키보드로 옮길 때만 칠한다 |
pressed 누름 — 트리거는 바탕 + 콘텐츠 축소, 선택지는 알약 + 콘텐츠 축소. 마우스 호버 · 선택지의 키보드 위치는 같은 바탕 · 알약(축소 없음) | spacing-x2 8px — 좌우 8 들어온 알약 — 글은 알약 안쪽 8 지점에서 시작한다 | color-bg-layer-floating-pressed #F7F8FD · 다크 #353B4D — 목록 바탕(bg-layer-floating) 위의 누름. 마우스 호버 · 키보드 위치도 같다 |
선택지 글자 · 아이콘 · 고른 표시
| 상태 | itemIcon 색 | itemLabel 글자·아이콘 | itemDescription 글자·아이콘 | indicator 색 | indicator 불투명도 |
|---|---|---|---|---|---|
enabled 기본 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | color-fg-neutral #1A1F2E · 다크 #F5F6FA — lucide check · 선 2.5 — 앱은 2.5 굵기 글꼴 LucideIcons.check500(Q-008-2) | — |
selected 고른 선택지 — 오른쪽 체크 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral #1A1F2E · 다크 #F5F6FA | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 | color-fg-neutral #1A1F2E · 다크 #F5F6FA — lucide check · 선 2.5 — 앱은 2.5 굵기 글꼴 LucideIcons.check500(Q-008-2) | 1 — 고르지 않은 선택지에는 체크를 그리지 않는다(자리도 없다) |
disabled 비활성 — 트리거 · 선택지 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | color-fg-disabled #8A91A0 · 다크 #838997 | — |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
누름 · 호버 — 바탕 | motion-duration-color-transition 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | background-color | |
누름 — 콘텐츠 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale | 트리거 · 선택지의 콘텐츠만 2px 거리 축소 — 모션 줄이기면 하지 않는다 |
셰브론 — 열 때 | motion-duration-d3 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | rotate | |
셰브론 — 닫을 때 | motion-duration-d2 100ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | rotate | |
목록 — 열 때 | motion-duration-d3 150ms | motion-ease-enter cubic-bezier(0, 0, 0.15, 1) | opacity, scale | 0.95 → 1 · 투명 → 불투명. 붙은 쪽(트리거 쪽)에서 커진다 |
목록 — 닫을 때 | motion-duration-d2 100ms | motion-ease-exit cubic-bezier(0.35, 0, 1, 1) | opacity, scale | 1 → 0.95 · 사라진다. 모션 줄이기면 투명도만 |
선택지 — 알약 | motion-duration-color-transition 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | background-color, inset | 알약 바탕이 나타나며 좌우 8 들어온다 |
오류 — 테두리 | motion-duration-d2 100ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | border-color |
Selection Mode
하나 고르기가 기본이다. 여럿 고르기(multiple)도 둔다.
- 하나 고르기 — 고르면 목록이 닫히고 값이 트리거에 들어간다. 고른 선택지를 다시 눌러도 풀리지 않는다 — "없음" 이 답이 될 수 있는 칸은 그 답을 선택지로 둔다(아래 "없음" 을 답으로 받기).
- 여럿 고르기 — 목록이 열린 채로 남아 이어서 고른다. 고른 선택지를 다시 누르면 풀린다. 트리거에는 고른 순서대로 쉼표로 잇고("식비, 교통"), 칸 폭을 넘으면 "첫 값 외 N개" 로 줄인다.
Prefix Icon
트리거와 선택지 앞에 아이콘을 둘 수 있다. 트리거의 아이콘은 어떤 종류의 값을 고르는 자리인지 함께 알리고 싶을 때 쓴다. 트리거에 실제로 그리는 아이콘은 고른 개수로 정한다(SEED).
- 하나를 골랐다 — 그 선택지의 아이콘을 그린다(트리거에 준 아이콘을 덮는다). 선택지에 아이콘이 없으면 트리거의 아이콘.
- 둘 이상 골랐다(여럿 고르기) — 트리거의 아이콘.
- 그릴 아이콘이 없으면 아이콘 없이 그린다.
선택지 아이콘은 묶음 안에서 모두 두거나 모두 빼고, 아이콘만으로 뜻을 알리지 않는다.
Content
목록은 트리거 폭 그대로, 트리거 아래 8 에 붙는다. 아래가 모자라면 위로 뒤집고, 화면 가장자리와는 8 을 둔다(안전 영역이 있으면 그 값). 높이는 480 과 목록이 놓이는 쪽에 남은 화면 중 작은 값이고(남은 화면이 200 보다 좁아도 200 은 둔다), 넘치면 목록 안에서 스크롤한다. 놓이는 쪽은 아래에 다 들어가면 아래, 아니면 트리거 아래 · 위 중 넓은 쪽이고, 남은 화면은 트리거에서 그쪽 화면 가장자리 8(안전 영역이 있으면 그 값)까지다. 열면 초점이 목록으로 가 화면 키보드는 내려간다 — 남은 화면에 키보드를 세지 않는다(구현 세션 질문 Q-008-1 — SEED "가용 높이" 와 같은 뜻). 480 은 listMaxHeight 로 줄일 수 있다 — Table Pagination 의 범위 목록만 240(SEED)을 쓴다. 열면 고른 선택지가 보이게 스크롤된 채로 열린다 — 선택지가 많은 Select 는 목록 안에서 스크롤되는 모습을 가정하고 짠다.
선택지 글은 목록 안에서는 줄바꿈되고(자르지 않는다), 트리거에서는 한 줄로 말줄임된다.
목록
| 속성 | 값 | 비고 |
|---|---|---|
| 너비 | 트리거 폭 | 트리거와 같은 폭 — 선택지 글은 목록 안에서 줄바꿈된다 |
| z-index | 200 | z-floating — specs/z-index.md L3. 대화상자 · 시트(101) 안에서 열어도 그 위 |
| 배경 | color-bg-layer-floating #FFFFFF · 다크 #2D3346 | |
| 모서리 | radius-r5 20px | |
| 그림자 | shadow-s3 | |
| 상하 여백 | spacing-x2 8px | |
| 간격 | spacing-x2 8px | 묶음 사이(위아래 8 + 선 1px + 8) |
| 위 바깥 여백 | spacing-x2 8px | 트리거와의 거리 — 아래가 모자라면 위로 뒤집는다 |
| 바깥 여백 | spacing-x2 8px | 화면 가장자리와 최소 거리(안전 영역이 있으면 그 값) |
| 최대 높이 | min(480px, 남은 화면) | 남은 화면 = 목록이 놓이는 쪽(아래에 다 들어가면 아래, 아니면 아래 · 위 중 넓은 쪽)의 트리거 ~ 화면 가장자리 8. 화면 키보드는 세지 않는다 — 열면 초점이 목록으로 가 키보드가 내려간다(Q-008-1). 남은 화면이 200 보다 좁아도 200 은 둔다 — 넘치면 목록 안에서 스크롤. 열면 고른 선택지가 보이게 스크롤된다 |
Group
성격이나 정렬 기준이 다른 선택지는 묶음으로 나눈다. 묶음에는 제목을 붙일 수 있고(fg-neutral-subtle), 묶음이 둘 이상이면 사이에 1px 선(좌우 16 들임)을 저절로 그린다 — 묶음 사이는 8 + 1 + 8 = 17 이다. 선택지 사이에는 선을 긋지 않는다.
선택지
| 속성 | 값 | 비고 |
|---|---|---|
| 좌우 여백 | spacing-x4 16px | |
| 커서 | pointer | |
| 전환 대상 | background-color | 알약 바탕과 들임(inset)이 함께 |
| 전환 시간 | motion-duration-color-transition 150ms | |
| 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) |
묶음 제목
| 속성 | 값 |
|---|---|
| 글자·아이콘 | color-fg-neutral-subtle #62697A · 다크 #A2A8B7 |
| 좌우 여백 | spacing-x4 16px |
묶음 사이 선
| 속성 | 값 | 비고 |
|---|---|---|
| 색 | color-stroke-neutral-subtle #EDEFF3 · 다크 #353B4D | |
| 높이 | 1px | |
| 좌우 바깥 여백 | spacing-x4 16px | 좌우 16 들인다 |
| 아래 바깥 여백 | spacing-x2 8px |
Guidelines
늘 Field 의 라벨과 함께
Select 는 늘 Field 의 라벨과 함께 쓴다. 라벨이 없으면 고른 뒤에 그 값이 무엇인지 알 수 없다 — placeholder 는 고르면 사라지므로 라벨을 대신하지 못한다.
"없음" 을 답으로 받기
하나 고르기는 고른 선택지를 다시 눌러도 풀리지 않는다. 칸의 성격에 따라 셋으로 나눈다(SEED).
| 칸 | 어떻게 |
|---|---|
| "없음" 이 답이 될 수 있다 | "없음" 선택지를 둔다 — 글은 "{칸 이름} 없음" 한 가지 꼴("결제 수단 없음" — "해당 없음" · "선택 안 함" 이 아니다). 다른 선택지와 성격이 달라 맨 앞 따로 묶음에 둔다 |
| 하나를 꼭 골라야 한다 | "없음" 을 두지 않는다. 고르지 않고 제출하면 Field 의 오류로 알린다("결제 수단을 골라주세요.") |
| 고르지 않은 상태가 있을 수 없다 | "없음" 을 두지 않고 기본값을 미리 골라 둔다(정렬 기준 · 통화) |
"없음" 을 고르면 값을 고른 것으로 친다 — 트리거에 그 글을 보이고, 필수 칸이어도 검증을 통과한다. 그래서 질문을 건너뛴 것과 없다고 답한 것을 가를 수 있다. 저장된 기록을 수정으로 열 때 그 칸의 값이 비어 있으면(null) 트리거에 "{칸 이름} 없음" 을 보인다 — 저장한 기록의 빈 값은 "없다고 답한 것" 이다. 자리 글(placeholder)은 아직 아무것도 묻지 않은 새로 추가할 때만 보인다(2026-10-29 17-1 — 이 구분 규칙의 풀이). Select 에는 지우기 버튼을 두지 않는다(선택 사항인 Input Button 에만).
여럿 고르기
여럿 고르기는 다시 누르면 풀리므로 고른 것을 되돌리는 선택지가 따로 필요 없다. 대신 생김새로는 드러나지 않는 제약을 글로 알린다.
- 몇 개까지 고를 수 있는지 — Field 설명에 적는다("최대 3개까지 고를 수 있어요.").
- 꼭 골라야 하면 — 필수 표시를 하고, 하나도 고르지 않고 제출하면 Field 의 오류로 알린다.
- 다른 선택지를 바꾸는 선택지를 두지 않는다 — "전체 선택" 이나 나머지를 지우는 "없음" 은 결과를 짐작하기 어렵다. 전체 선택이 잦으면 Checkbox 의 부모 · 자식 묶음을 쓴다. 건너뛴 것과 없다고 답한 것을 갈라야 하면 앞에 Checkbox 를 두고("하자 있음"), 체크했을 때만 Select("하자 종류")를 보인다.
여럿 고른 값 보이기
트리거는 한 줄이다. 다 보이면 고른 순서대로 쉼표로 잇고("식비, 교통"), 칸 폭을 넘으면 개수로 줄인다 — 가장 먼저 고른 값을 남기고 나머지 개수를 붙인다("식비 외 2개"). "외" 는 앞의 값을 뺀 개수다 — "식비 등 2개" 라고 쓰면 모두 두 개라는 뜻이 되어 틀린다. 값이 서로 동등해 하나를 보여 주는 것이 도움이 안 되면 전체 개수로 쓴다("3개 고름").
고른 값이 조건으로 묶여 결과가 달라지는 자리(필터)는 쉼표 대신 접속사로 관계를 드러내고("식비 또는 교통"), 어떻게 묶이는지를 Field 설명에도 적는다.
선택지 순서
사용 빈도 · 크기 · 시간처럼 그 목록이 이미 가진 기준 하나로 일관되게 줄 세운다(가나다순을 기본으로 두지 않는다). 기준이 다른 선택지는 묶음으로 나눠 경계를 드러낸다.
선택지 글
- 명사형으로 짧게 — "서울 선택" 처럼 동작이나 부연을 붙이지 않는다. 선택지 사이에 낱말 · 길이 · 말투를 맞춘다.
- 코드값(
Y·N·ANNUAL· 아이디)을 글로 내지 않는다(Writing). - 트리거 폭에서 말줄임 없이 보이는지 확인한다 — 목록에서는 줄바꿈되어 다 보여도 트리거에서는 잘린다. 길면 글을 줄이는 것을 먼저 하고, 부연은 선택지 설명(한 줄)에 쓴다. 설명은 트리거에 보이지 않는다.
placeholder
고르기 전의 글은 어떤 종류의 값을 고르는지 알리는 "{값의 종류} 선택" 꼴이다("결제 수단 선택"). "여기를 눌러 펼쳐 보기" 같은 막연한 글이나 라벨만 그대로 둔 글은 쓰지 않는다. 마침표는 찍지 않는다.
폰에서도 칸 아래 목록
폰에서도 목록은 칸 아래에 붙는다 — 시트로 바꾸지 않는다(SEED). 짧은 선택지를 화면을 덮는 시트로 열면 손이 더 가고 폼이 가려진다. 아래가 모자라면 위로 열린다. 시트가 필요한 것 — 달력 · 시각 · 아이콘 격자 · 검색해 고르는 긴 목록 — 은 Select 가 아니라 Input Button 이 연다.
고르는 컴포넌트 고르기
고르는 컴포넌트를 고르는 표는 여기 한 곳이다 — Radio · Checkbox · Switch · Chip · Select Box · Segmented Control · Tabs · List · Searchable List · Wheel Picker · Menu 문서는 선택지 개수를 다시 적지 않고 이 절을 가리키며, 자기 짝 비교 표만 둔다. 선택지 개수의 숫자(2 ~ 4 · 5 이상)도 아래 판단 순서 하나다(2026-10-22 사용자 결정 1B · 2B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym).
먼저 — 이 자리는 무슨 일을 하나
| 하는 일 | 고를 것 | Desk 예 |
|---|---|---|
| 폼에 넣을 값을 고른다 — 저장 · 다음을 눌러야 반영 | 아래 판단 순서로 — Select Box · Select · Chip · Radio · Checkbox · Input Button | 거래 추가의 결제 수단 · 반복 종료 · 공개 범위 |
| 보이는 내용을 바로 거르거나 정렬하거나 다르게 본다 | 2 ~ 4가지 고정 보기 → Segmented Control(내용 바로 위, 한 화면 하나) · 조건을 여럿 걸고 푼다 · 개수가 바뀐다 → Chip 필터 바 | 할 일 오늘 · 이번 주 · 전체 · 완료 / 거래 검색의 카테고리 · 기간 · 금액 |
| 다른 구역 · 화면으로 옮긴다 | Tabs — 1차 Line · 2차 Chip Tabs | 가계부 · 자산 · 통계 · 예산 |
| 기능 하나를 켜고 끈다 | 누르는 순간 바뀐다 → Switch(설정 줄이면 List 스위치 줄) · 저장해야 반영 → Checkbox | 결제 알림 · 고정 금액으로 기록 |
| 화면 폭 목록에서 하나를 고르고 바로 바뀐다 | List 라디오 줄 | 기본 통화 · 테마 |
| 누르면 동작을 실행한다 | Menu(1280 이상) · Menu Sheet(1280 미만) — 값을 고르는 데 쓰지 않는다 | 거래 상세 머리 ⋮ → 복사해 새로 쓰기 · 삭제 |
- 결제 알림결제 예정일 하루 전 · 당일
- 대한민국 원KRW
- 미국 달러USD
폼 값 — 위에서부터 판단한다
선택지마다 필요한 정보의 양, 선택지 개수, 글 길이로 고른다. 위에서부터 판단한다(SEED — 2026-10-01 사용자 결정). "이럴 때 더 맞다" 열은 SEED Radio 문서의 판단 기준이다.
| 순서 | 이런 선택지 | 컴포넌트 | 이럴 때 더 맞다 | 제품의 자리 |
|---|---|---|---|---|
| 1 | 그림 · 여러 줄 설명 · 딸린 입력이 필요하다 | Select Box | 나란히 견주고 버튼으로 확정한다 | 반복 거래 종료 · 분배 방식 · 내보내기 형식 · 휴가 부여 방법 |
| 2 | 5개 이상이거나, 한 줄 설명이면 충분하다 | Select | 사용자가 답을 이미 안다 · 선택지가 늘어날 수 있다 · 자리가 좁다 | 결제 수단 · 통화 · 휴가 정책 · 계좌 종류 · 반복 단위 · 반복 요일 |
| 3 | 2 ~ 4개, 설명 없이 글이 짧다 | Chip | 한 줄에서 가볍게 — 고른 값이 곧 화면의 갈래일 때 | 거래 종류 · 우선순위 · 알림 소리 |
| 3 | 2 ~ 4개, 설명 없이 글이 길다 | Radio · Checkbox | "하나의 정답을 고른다" · "해당되는 것에 모두 체크한다" — 서류처럼 차분히 견줄 때 | 공개 범위 · 동의. 예 / 아니오 하나는 Checkbox |
| 4 | 너무 많아 스크롤로 찾기 어렵다 | Input Button + 검색 시트(Searchable List) | 이름을 알고 친다 | 결재자 · 사람 · 종목 · 카드사 |
- 요일 7개도 2 — 여럿 고르는 Select 다("월, 수, 금").
- 폼 값을 탭(Segmented Control · Tabs)으로 고르지 않는다 — 탭은 보이는 내용을 바로 거르거나 바꾸는 자리다. 폼 값의 탭은 Chip · Select 로 옮긴다.
예외 · 주의
- 저장 폼 안의 Switch — 값이 저장 때 들어가도 누르면 화면이 바로 바뀌면 Switch 다(일정 "종일" — 날짜 칸이 바뀐다). Switch 결정(누르는 순간만) 안의 경계다 — 경계 사례는 Checkbox 의 "Checkbox 와 Switch" 표.
- 거르기 칩은 개수 규칙이 다르다 — 폼 값 칩은 2 ~ 4(판단 순서 3), 거르기 칩은 개수가 바뀌거나 5개 이상인 축(카테고리 · 태그)에 쓴다(Chip 필터 바). 2 ~ 4가지로 고정된 보기는 Segmented Control.
- 늘어날 수 있는 목록 — 지금 셋이어도 사용자가 더할 수 있는 목록(카테고리 · 결제 수단)은 Select 다(SEED Radio "앞으로 더 늘어날 가능성").
- 선택지가 하나면 Checkbox — 설명 · 딸린 입력이 붙어도("고정 금액 사용").
- 글이 칸보다 길면 다른 컴포넌트 — Segmented Control 칸 글이 줄을 바꾸면 Chip · Select 로.
- 컨트롤을 끼운 줄(스위치 · 체크 · 라디오)에는 다른 버튼을 두지 않는다 — 줄 전체가 라벨이다(List).
- Menu 로 값을 고르지 않는다 — 테마는 List 라디오 줄, 정렬 · 보기는 Segmented Control · Select. 고른 표시가 없는 목록으로 값을 고르면 지금 값이 안 보인다.
Select 와 Menu
Menu 는 누르는 순간 실행되는 명령 목록이고, Select 는 폼에 넣을 값을 고르는 입력이다.
| Select | Menu(그 차례에) | |
|---|---|---|
| 목적 | 폼에 넣을 값 고르기 | 동작 실행(정렬 · 삭제 · 공유) |
| 반영 | 고른 뒤 저장 · 제출해야 반영 | 누르면 바로 실행 |
| Field | Field 와 함께(라벨) | Field 없이 버튼에서 연다 |
| 고른 표시 | 오른쪽 체크 | 없다(누르면 닫히고 실행) |
코드
레시피 recipes/shadcn/components/ui/select.tsx(Select · SelectGroup · SelectItem)를 Field 안에 둔다. 선택지는 글(label)을 속성으로 받는다 — 목록이 닫혀 있어도 트리거 · 글자로 찾기(typeahead)가 그 글을 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.
기본
import { Field } from "@/components/ui/field"
import { Select, SelectItem } from "@/components/ui/select"
<Field label="통화">
<Select placeholder="통화 선택" value={currency} onValueChange={setCurrency}>
<SelectItem value="KRW" label="원" description="KRW" />
<SelectItem value="USD" label="미국 달러" description="USD" />
<SelectItem value="JPY" label="일본 엔" description="JPY" />
<SelectItem value="EUR" label="유로" description="EUR" />
<SelectItem value="CNY" label="중국 위안" description="CNY" />
</Select>
</Field>묶음 · 아이콘 · "없음"
import { Banknote, CircleSlash, CreditCard, Landmark } from "lucide-react"
import { Select, SelectGroup, SelectItem } from "@/components/ui/select"
<Field label="결제 수단">
<Select placeholder="결제 수단 선택" value={asset} onValueChange={setAsset}>
<SelectGroup>
<SelectItem value="none" label="결제 수단 없음" prefixIcon={<CircleSlash />} />
</SelectGroup>
<SelectGroup label="카드">
<SelectItem value="hyundai-m" label="현대카드 M" prefixIcon={<CreditCard />} />
<SelectItem value="shinhan-deep" label="신한카드 Deep" prefixIcon={<CreditCard />} />
</SelectGroup>
<SelectGroup label="계좌 · 현금">
<SelectItem value="kb" label="국민 주계좌" description="123-45-6789" prefixIcon={<Landmark />} />
<SelectItem value="cash" label="현금" prefixIcon={<Banknote />} />
</SelectGroup>
</Select>
</Field>여럿 고르기
<Field label="반복 요일" description="고른 요일마다 거래를 만들어요.">
<Select multiple placeholder="요일 선택" value={days} onValueChange={setDays}>
{["월", "화", "수", "목", "금", "토", "일"].map((d) => (
<SelectItem key={d} value={d} label={`${d}요일`} />
))}
</Select>
</Field>value 는 고른 순서대로 쌓인다. 트리거 글은 formatValue 로 바꾼다 — 기본은 "월요일, 수요일" · 넘치면 "월요일 외 2개".
상태
<Field label="휴가 정책" invalid errorMessage="휴가 정책을 골라주세요.">
<Select placeholder="휴가 정책 선택">…</Select>
</Field>
<Field label="통화" disabled>
<Select defaultValue="KRW">…</Select>
</Field>
<Field label="부서" readOnly>
<Select defaultValue="design">…</Select>
</Field>Behavior
| 인터랙션 | 동작 |
|---|---|
| 트리거 Click / Tap | 목록을 열고 닫는다. 포인터로 열면 고른 선택지가 보이게 스크롤만 하고 미리 짚지 않는다. |
트리거 ↓ ↑ Enter Space | 목록을 열고 고른 선택지(없으면 첫 선택지)를 짚는다. |
| 트리거 글자 키(닫힌 채) | 하나 고르기면 그 글자로 시작하는 다음 선택지로 값을 바로 바꾼다(열지 않는다 — 네이티브 select 와 같다). |
<label> 누르기 | 트리거로 포커스만 옮긴다 — 목록은 열지 않는다(Field). |
목록 ↓ ↑ | 다음 · 이전 선택지(막힌 선택지는 건너뛴다). Home · End 는 처음 · 끝. 글자 키는 그 글자로 시작하는 선택지로. |
목록 Enter · Space · 선택지 누르기 | 하나 고르기 — 고르고 닫는다. 여럿 고르기 — 고르거나 풀고 열어 둔다. |
Esc · 바깥 누르기 · Tab | 닫는다. 고른 값은 그대로다. Esc 로 닫으면 트리거로 포커스가 돌아온다. |
| 마우스 호버 | 짚은 선택지가 따라간다(알약). 터치는 짚지 않는다 — 손가락 아래 미리 칠한 알약은 눌린 채 멈춘 것처럼 보인다. |
| Disabled | 포커스 · 열기 불가. 커서 not-allowed. |
| Readonly | 포커스는 되고 열리지 않는다. 값은 그대로 읽힌다. |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 트리거 값 fg-neutral 16.41 · 13.42, 읽기 전용 값 15.20 · 10.32. placeholder 4.18 · 4.13(누름 바탕 위 3.94 · 3.18) — 4.5:1 밑, 사용자가 대가를 알고 SEED 값을 골랐다(2026-10-16 글자 역할 D7, DESIGN v119). 목록 글 fg-neutral 16.41 · 11.62, 묶음 제목 · 설명 fg-neutral-subtle 5.50 · 5.27(알약 위 5.18 · 4.68) ✓. 비활성 fg-disabled 는 기준 밖(비활성 UI) |
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 트리거 키보드 포커스 링 stroke-focus-ring Desk 8.38 · 6.10 · HR 8.17 · 6.07, 오류 테두리 5.06 · 6.08, 고른 표시 체크 16.41 · 11.62 ✓. 누름 · 호버 · 키보드 위치 알약은 목록 바탕과 1.06 · 1.13 ⚠ — SEED 그대로 둔다(2026-10-01 사용자 결정, 키보드 위치에 링을 더하는 안은 고르지 않았다). 짚은 선택지는 화면 읽기 프로그램에 aria-activedescendant 로 알린다. 기본 1px 테두리(1.23 · 1.56)는 라벨 · 셰브론이 칸을 함께 알린다(SEED 와 같다) |
| WCAG 2.4.7 Focus visible | 트리거는 키보드 포커스에 링. 목록 안 키보드 위치는 알약(SEED — 위 1.4.11 의 ⚠) |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 트리거 · 선택지 모든 크기 ✓(가장 작은 선택지 medium 39) |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 트리거 large 52 ✓ · 선택지 large 46 ✓. medium 40 · 39 ⚠ — 1280 이상 데스크톱 웹(마우스)에서만(예외 — Table Pagination 의 고르기) |
| ARIA | 트리거 <button role="combobox" aria-haspopup="listbox" aria-expanded aria-controls> — 이름은 Field 의 라벨(<label for>), 라벨이 없으면 aria-label. 오류 aria-invalid, 필수 aria-required, 읽기 전용 aria-readonly. 목록 role="listbox"(여럿이면 aria-multiselectable) · 짚은 선택지는 aria-activedescendant, 선택지 role="option" aria-selected(막히면 aria-disabled), 묶음 role="group" + 제목 aria-labelledby. 앱은 listbox · option 대신 — Flutter SemanticsRole 에 listbox · option 이 없다. 하나 고르기 선택지는 Semantics(button: true, selected: 고름, label: 제목 + 설명)("원, 선택됨, 버튼"), 여럿 고르기는 checked: 고름("체크됨 · 체크 안 됨")을 더한다. 목록 노드는 역할을 지어내지 않고 이름만 둔다("목록 안 · n개 중 m번째" 는 스크롤 자식 수로 OS 가 붙인다). 이 꼴을 앱 테스트로 고정한다 — 선택지를 라디오(checked + inMutuallyExclusiveGroup)로 바꾸는 안은 고르지 않았다(2026-10-17 사용자 결정 — 03-B 결정 2 A) |
Do / Don't
✅ Do
- 모든 Select 를 Field 로 감싸 라벨을 단다.
- placeholder 는 "{값의 종류} 선택".
- "없음" 이 답이 될 수 있으면 "{칸 이름} 없음" 을 맨 앞 따로 묶음에.
- 여럿 고르기는 최대 개수를 Field 설명에.
- 선택지 글은 명사형으로 짧게, 부연은 한 줄 설명에.
- 폰에서도 칸 아래 목록 — 시트는 Input Button 으로.
❌ Don't
- placeholder 를 라벨 대신 쓰기.
- "해당 없음" · "선택 안 함" 처럼 칸마다 다른 "없음" 글, 지우기 버튼으로 "없음" 받기.
- "전체 선택" 처럼 다른 선택지를 바꾸는 선택지.
- 2 ~ 4개 짧은 선택지를 Select 로 숨기기(Chip · Radio), 검색이 필요한 긴 목록을 Select 로.
- 폼 값을 탭으로 고르기.
- 폰에서 medium(40) 쓰기.
Specification
select.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Select 를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 배경background | transparent |
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757 | ||
| 테두리 두께borderWidth | 1px상자 안쪽(inset). 오류의 2px 는 안쪽에 덧그린다 | ||
| 커서cursor | pointer | ||
| 전환 대상transitionProperty | background-color | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
value | enabled | 값 글자색foreground | fg-neutral#1A1F2E다크 #F5F6FA |
| 굵기fontWeight | 400 | ||
placeholder | enabled | placeholder 색foreground | fg-placeholder#767C8B다크 #838997 |
| 굵기fontWeight | 400 | ||
prefixIcon | enabled | 앞 아이콘 색color | fg-neutral-muted#535866다크 #B7BDCC |
chevron | enabled | 색color | fg-neutral-muted#535866다크 #B7BDCC |
content | enabled | 최대 높이maxHeight | min(480px, 남은 화면)남은 화면 = 목록이 놓이는 쪽(아래에 다 들어가면 아래, 아니면 아래 · 위 중 넓은 쪽)의 트리거 ~ 화면 가장자리 8. 화면 키보드는 세지 않는다 — 열면 초점이 목록으로 가 키보드가 내려간다(Q-008-1). 남은 화면이 200 보다 좁아도 200 은 둔다 — 넘치면 목록 안에서 스크롤. 열면 고른 선택지가 보이게 스크롤된다 |
| 너비width | 트리거 폭트리거와 같은 폭 — 선택지 글은 목록 안에서 줄바꿈된다 | ||
| 상하 여백paddingY | spacing-x28px | ||
| 바깥 여백margin | spacing-x28px화면 가장자리와 최소 거리(안전 영역이 있으면 그 값) | ||
| 위 바깥 여백marginTop | spacing-x28px트리거와의 거리 — 아래가 모자라면 위로 뒤집는다 | ||
| 간격gap | spacing-x28px묶음 사이(위아래 8 + 선 1px + 8) | ||
| 모서리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. 대화상자 · 시트(101) 안에서 열어도 그 위 | ||
item | enabled | 좌우 여백paddingX | spacing-x416px |
| 커서cursor | pointer | ||
| 전환 대상transitionProperty | background-color알약 바탕과 들임(inset)이 함께 | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
itemIcon | enabled | 색color | fg-neutral#1A1F2E다크 #F5F6FA |
itemLabel | enabled | 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA |
| 굵기fontWeight | 400 | ||
itemDescription | enabled | 간격gap | spacing-x0_52px글 ↔ 설명 |
| 글자·아이콘foreground | fg-neutral-subtle#62697A다크 #A2A8B7 | ||
indicator | enabled | 색color | fg-neutral#1A1F2E다크 #F5F6FAlucide check · 선 2.5 — 앱은 2.5 굵기 글꼴 LucideIcons.check500(Q-008-2) |
highlight | enabled | 모서리radius | radius-r312px |
| 배경background | transparent누를 때 · 키보드로 옮길 때만 칠한다 | ||
groupLabel | enabled | 좌우 여백paddingX | spacing-x416px |
| 글자·아이콘foreground | fg-neutral-subtle#62697A다크 #A2A8B7 | ||
divider | enabled | 높이height | 1px |
| 아래 바깥 여백marginBottom | spacing-x28px | ||
| 좌우 바깥 여백marginX | spacing-x416px좌우 16 들인다 | ||
| 색color | stroke-neutral-subtle#EDEFF3다크 #353B4D | ||
trigger | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4DSEED bg.transparent-pressed 의 불투명 짝. 마우스는 호버에 같은 바탕 |
| 배율scale | 콘텐츠 2px 거리 축소값 · 아이콘만 준다(SEED scaleScope: content) — 테두리 · 바탕은 그대로 | ||
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
item | pressed | 배율scale | 콘텐츠 2px 거리 축소누르는 동안만(호버 · 키보드 위치는 축소 없음) |
highlight | pressed | 바탕 들임insetX | spacing-x28px좌우 8 들어온 알약 — 글은 알약 안쪽 8 지점에서 시작한다 |
| 배경background | bg-layer-floating-pressed#F7F8FD다크 #353B4D목록 바탕(bg-layer-floating) 위의 누름. 마우스 호버 · 키보드 위치도 같다 | ||
focusRing | focused | 링 두께width | 2px |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84트리거 — 키보드 포커스에만(마우스 · 터치로 눌러서는 없다) | ||
| 링 간격offset | 2px | ||
chevron | open | 회전rotate | 180deg |
indicator | selected | 불투명도opacity | 1고르지 않은 선택지에는 체크를 그리지 않는다(자리도 없다) |
trigger | invalid | 테두리borderColor | stroke-critical-solid#D72323다크 #FF84771px 위에 2px 가 나타난다(100ms) |
| 테두리 두께borderWidth | 2px | ||
trigger | disabled | 배경background | bg-disabled#F5F6FA다크 #353B4D흐리게 하지 않는다(v106) |
| 커서cursor | not-allowed | ||
value | disabled | 값 글자색foreground | fg-disabled#8A91A0다크 #838997 |
placeholder | disabled | placeholder 색foreground | fg-disabled#8A91A0다크 #838997 |
prefixIcon | disabled | 앞 아이콘 색color | fg-disabled#8A91A0다크 #838997 |
chevron | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
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 |
indicator | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
trigger | readonly | 배경background | bg-disabled#F5F6FA다크 #353B4D값은 진한 글자 그대로 — 열리지 않는다 |
| 커서cursor | default |
size=large
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 최소 높이minHeight | 52px |
| 좌우 여백paddingX | spacing-x416px | ||
| 간격gap | spacing-x2_510px | ||
| 모서리radius | radius-r312px | ||
value | enabled | 값 글자typography | text-t516px / 22px |
placeholder | enabled | 글자typography | text-t516px / 22px |
prefixIcon | enabled | 앞 아이콘size | 20px |
chevron | enabled | 크기size | 20px |
item | enabled | 높이height | 46px한 줄 — 설명이 있으면 66 |
| 상하 여백paddingY | spacing-x312px | ||
| 간격gap | spacing-x312px | ||
itemIcon | enabled | 크기size | 22px |
itemLabel | enabled | 글자typography | text-t516px / 22px |
itemDescription | enabled | 글자typography | text-t313px / 18px / 400 |
indicator | enabled | 크기size | 14px |
groupLabel | enabled | 상하 여백paddingY | spacing-x2_510px |
| 글자typography | text-t414px / 19px / 400 | ||
| 굵기fontWeight | 500 |
size=medium
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 최소 높이minHeight | 40px |
| 좌우 여백paddingX | spacing-x3_514px | ||
| 간격gap | spacing-x28px | ||
| 모서리radius | radius-r28px | ||
value | enabled | 값 글자typography | text-t414px / 19px |
placeholder | enabled | 글자typography | text-t414px / 19px |
prefixIcon | enabled | 앞 아이콘size | 16px |
chevron | enabled | 크기size | 16px |
item | enabled | 높이height | 39px한 줄 — 설명이 있으면 57 |
| 상하 여백paddingY | spacing-x2_510px | ||
| 간격gap | spacing-x28px | ||
itemIcon | enabled | 크기size | 18px |
itemLabel | enabled | 글자typography | text-t414px / 19px |
itemDescription | enabled | 글자typography | text-t212px / 16px / 400 |
indicator | enabled | 크기size | 12px |
groupLabel | enabled | 상하 여백paddingY | spacing-x28px |
| 글자typography | text-t313px / 18px / 400 | ||
| 굵기fontWeight | 400 |
size=responsive
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 바뀌는 폭breakpoint | 1280px미만은 large, 이상은 medium 의 값을 쓴다(SEED lg) — 앱은 늘 large |
SEED 와 다른 점
- 누름 · 호버 · 키보드 위치 바탕은 불투명한 색 — 트리거
bg-layer-default-pressed, 목록 안 알약bg-layer-floating-pressed. SEEDbg.transparent-pressed는 투명도가 있어 대비 검사기가 받지 않는다(v102). 묶음 사이 선도 불투명한stroke-neutral-subtle(SEEDstroke.neutral-muted는 투명도가 있다). - 그림자는 groo
shadow-s3(v105 — SEED 와 구조는 같고 값은 groo). - 아이콘은 lucide 선 아이콘(v106) — 셰브론
chevron-down, 고른 표시check(선 2.5). SEED 는 채운 체크다. - 선택지 커서는 pointer — SEED 는 default. groo 의 누르는 줄(List · Select Box)과 맞춘다.
- 트리거 이름은 라벨 하나 — SEED React 는 트리거에 고른 값을 따로 읽히지 않는다. groo 는
role="combobox"의 값으로 고른 글이 읽힌다(라벨 + 값).
Migration notes
2026-10-30 — 저장된 빈 값은 "없음"
"없음 을 답으로 받기" 의 풀이 한 줄을 더했다 — 수정으로 연 기록의 빈 값(null)은 "{칸 이름} 없음", 자리 글은 새로 추가할 때만(2026-10-29 17-1 에서 웹 · 앱에 시킨 규칙). 값 변경 없음. 옛 스펙은 select.history/v-pre-cleanup-1030.md.
2026-10-25 — 목록 줄 ⋮ 를 걷은 결정에 맞춤
"고르는 컴포넌트 고르기" 표의 Menu 예 "거래 줄 ⋮ → 수정 · 복사 · 삭제" 를 "거래 상세 머리 ⋮ → 복사해 새로 쓰기 · 삭제" 로 — 목록 줄에는 ⋮ 를 두지 않는다(Menu 의 "줄의 동작", 2026-10-25 사용자 결정). 값 변경 없음.
2026-10-22 — 첫 갈래 그림
"먼저 — 이 자리는 무슨 일을 하나" 표에 그림 first-guide 를 더했다(고르는 법 1B 의 비용 줄 "사이트 그림 pick-guide 에 첫 갈래 그림 하나" — 스펙 PR1 에서 남은 것). 판단 순서 그림 pick-guide 는 그대로다. 값 변경 없음.
2026-10-22 — 고르는 컴포넌트 고르기를 한 곳에
"고르는 컴포넌트 고르기" 를 넓혀 고르는 표를 이 절 한 곳에 모았다 — 첫 갈래(이 자리는 무슨 일을 하나) 표, 판단 순서(10-01 결정 표에 SEED Radio 의 "이럴 때 더 맞다" 열), 예외 · 주의. 다른 문서(Radio · Select Box · Segmented Control · Tabs · Searchable List · Wheel Picker · Menu · Checkbox · Switch)는 선택지 개수를 지우고 이 절을 가리키며 자기 짝 비교 표만 둔다. 선택지 개수는 이 표의 2 ~ 4 · 5 이상 하나다 — Radio "2 ~ 5 · 6 이상" · Select Box "2 ~ 6 · 7 이상" · Wheel Picker "5개 이하" 를 걷었다(사용자 결정 1B · 2B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym). 값 변경 없음.
2026-10-17 — 앱 선택지 낭독(03-B 결정 2 A)
스펙이 웹 ARIA(listbox · option)만 적고 앱의 의미 트리를 말하지 않았다(조사 03B-004 · 005). 사용자가 비교 페이지에서 "지금 그대로"(A)를 골랐다 — 앱 PSelect 선택지(p_select.dart:933~940)의 Semantics(container, button: true, selected, checked: multiple ? selected : null, label) 를 스펙 줄로 적었다(Accessibility 의 ARIA). 코드는 바뀌지 않는다 — 앱은 하나 고르기 선택지가 button + selected · 여럿 고르기가 checked 인지 위젯 테스트로 고정한다. 하나 고르기를 라디오로 바꾸는 안(B)은 고르지 않았다. 옛 스펙은 select.history/v-pre-03e-03b.md.
2026-10-15 — 남은 화면 · 앱의 체크 굵기
구현 세션 질문 두 가지를 있는 규칙으로 정리했다.
- "남은 화면" 은 목록이 놓이는 쪽의 트리거 ~ 화면 가장자리 8(Q-008-1). 웹(Radix 의 가용 높이) · 앱이 이미 그렇게 잰다. 열면 초점이 목록으로 가 입력 칸의 키보드가 내려가므로, 목록이 떠 있는 동안 키보드가 올라와 있는 경우는 없다 — 키보드를 세는 규칙을 두지 않는다. "Content" 절 문장을 고쳤다.
- 고른 표시 check 선 2.5 는 그대로(Q-008-2). 앱은 굵기를 바꿀 수 있다 — lucide 의 2.5 굵기 글꼴(
LucideIcons.check500)을 쓴다. 앱이 이미 탭 바 · 떠 있는 버튼에서 같은 글꼴(house500·plus500)을 쓴다.
2026-10-01 — SEED Select 로
사용자가 비교 페이지에서 여덟 가지를 모두 SEED 로 정했다 — Select(짧은 선택지 5개 이상 · 칸 아래 목록)와 Input Button(달력 · 시각 · 아이콘 격자 · 긴 목록을 시트 · 팝오버로)으로 나눈다 · 폰에서도 칸 아래 목록 · 목록 모양 SEED 그대로 · "없음" 은 "{칸 이름} 없음" 선택지(맨 앞 따로 묶음, Select 에 지우기 버튼 없음) · 여럿 고르기를 둔다 · 고르는 컴포넌트 순서 SEED 그대로(요일 7개도 Select, 폼 값의 탭은 걷는다) · Input Button 은 1280 미만 시트 · 이상 팝오버, 달력 · 시각은 "완료" 로 확정 · 긴 목록은 Input Button + 검색 시트(Combobox 를 두지 않는다). 옛 스펙은 select.history/v-pre-seed-select.* 에 남겼다.
| 옛 Select | 새 Select |
|---|---|
트리거 40 하나 · 모서리 4 · 회색 채운 바탕(surface-input) + 1px border-default | large 52(모서리 12) · medium 40(8) · 반응형 · 투명 바탕 + 안쪽 1px stroke-neutral-weak — Input 과 같은 상자 |
셰브론 16 text-tertiary, 열려도 그대로 | 20 · 16 fg-neutral-muted, 열리면 180° |
| 포커스: 브랜드 테두리 + 30% 링 · 비활성 흐림 50% | 키보드 포커스 링 2px · 띄움 2 · 오류 안쪽 2px · 비활성 · 읽기 전용 bg-disabled(흐림 없음) |
목록: 모서리 4 · 1px 테두리 · shadow-md · 위아래 4 · 폭은 트리거 이상 · 최대 384 | 모서리 20 · bg-layer-floating · shadow-s3 · 위아래 8 · 트리거 폭 그대로 · 아래 8 · 최대 min(480, 남은 화면) |
| 선택지 32 · 왼쪽 체크(32 들임) · 짚으면 채운 줄 | large 46 · medium 39 · 오른쪽 체크(고른 것만 — 자리도 없다) · 누름 · 호버 · 키보드 위치는 좌우 8 들인 알약 |
| 하나만 고른다 | 하나 · 여럿(multiple) — "식비, 교통" · "식비 외 2개" |
| Radix Select 조각(SelectTrigger · SelectValue · SelectContent · SelectLabel · SelectSeparator · 스크롤 버튼) | Select(트리거 · 목록) · SelectGroup label · SelectItem value label description prefixIcon — 묶음 사이 선은 저절로 |
레시피는 Radix Select 대신 Radix Popover 위에 목록(role="listbox")을 직접 짰다 — Radix Select 는 여럿 고르기를 받지 않는다.
제품은 앱 적용 단계에서 옮긴다(2026-10-01 조사 — 세 제품 코드를 읽고 일부는 크로미움에 띄워 쟀다).
- Desk 웹 — Select 29(40 · 글자 15 · 모서리 4 · 채운 바탕, Field 라벨 연결 0/29) · 날짜 14 · 시각 6 · 월 고르기 7 · 폼 값을 고르는 Tabs 16. 고르는 칸 높이 40 / 36 / 33.5 / 32 / 28 / 20, 셰브론 7가지(하나도 안 돈다), 목록 4가지, "없음" 글 8가지(선택 안 함 · 일시불 · 가져오지 않음 · 최상위 카테고리로 두기 · 라벨이 없습니다 · 태그 없음 · 반복 없음 · 없음 — 맨 앞 · 맨 뒤 섞임). 다크에서 목록 그림자가 라이트 값, 키보드 위치 1.12:1, 키보드로 못 쓰는 고르기 4.
- Desk 앱 — PSelect 24(칸 43 · 40 · 모서리 4) · 값 고르기 탭 15 · 칩 · 타일 단일 15무리 · 다중 11무리. 고른 상태 모양 17가지. 목록에 없는 값이면 칸이 비고 값은 그대로 저장된다(D1), 숨겨진 자산이 "선택 안 함" 으로 보이며 저장된다(D2), 반복 폼이 낡은 값을 저장한다(D3), 비활성 PSelect 가 활성과 같다(D8), 영어 모드에 한국어(D10 · D11). 계좌 종류 6칸 탭은 "마이너스통장" 이 어느 폰 폭에도 안 들어간다.
- HR 웹 — Select 72(36 · 글자 14, 폭 세 가지, Field 라벨 연결 11/72) · 사람 고르기 7(검색 없음) · 역할 콤보박스 1. 예 / 아니오를 Select 로 받는 칸 3(→ Checkbox), 코드값(
Y·N·ANNUAL· user_id)이 글로 보인다, 2 ~ 4개 폼 값 16칸이 모두 Select(→ Chip). 테두리 대비 1.16 · 셰브론 1.91.
Select Box
테두리 상자로 선택지를 보여 주고 하나 · 여럿을 고르게 하는 컴포넌트. 설명 · 그림 · 딸린 입력이 붙는 선택지를 비교해 고르고, 저장 · 다음 같은 버튼으로 반영하는 자리에 쓴다.
Side Navigation
데스크톱(768 이상)의 주 메뉴 — 화면 왼쪽의 세로 띠(사이드바)에 서비스의 최상위 화면들을 묶어 둔다. 1280 이상은 펼침 240, 768 ~ 1279 는 아이콘만 56 으로 저절로 접히고, 손으로 접고 펼 수 있다. 768 미만에서는 없다 — Desk 웹은 Bottom Navigation, HR 은 ☰ 로 여는 왼쪽 Side Panel 에 같은 항목을 담는다. 다른 이동 부품(Top Navigation · Tabs)과 고르는 표는 DESIGN.md Layout 의 "이동 부품 고르기" 한 곳이다.