Select Box
테두리 상자로 선택지를 보여 주고 하나 · 여럿을 고르게 하는 컴포넌트. 설명 · 그림 · 딸린 입력이 붙는 선택지를 비교해 고르고, 저장 · 다음 같은 버튼으로 반영하는 자리에 쓴다.
구조는 당근 SEED Select Box(Apache-2.0)를 따른다 — 하나 고르기(Radio Select Box) · 여럿 고르기(Check Select Box) · 묶음(Select Box Group). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-01 사용자 결정).
수치 원본은 select-box.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
고르기 방식 · 컨트롤 · 열 수 · 앞 · 설명 · 펼침 · 상태를 고르면 스펙대로 그린 상자와 그 코드가 바뀐다. 실제로 눌러 볼 수 있다.
import { RadioSelectBox, RadioSelectBoxGroup } from "@/components/ui/select-box"
<RadioSelectBoxGroup defaultValue="count" aria-label="종료">
<RadioSelectBox value="none" label="무기한" description="중지할 때까지 계속 반복" />
<RadioSelectBox value="count" label="횟수 지정" description="정한 횟수만큼 반복" footer={<Input aria-label="반복 횟수" defaultValue={12} />} />
<RadioSelectBox value="date" label="종료일 지정" description="정한 날까지 반복" />
</RadioSelectBoxGroup>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Prefix | 앞 — 아이콘 22 · 그림(테마 미리보기 같은). 없어도 된다. |
| ⓑ Title | 제목 — 16 · 500. 짧게. 옆에 배지를 둘 수 있다. |
| ⓒ Description | 설명 — 13 · 진한 회색. 두 줄까지. |
| ⓓ Control | 오른쪽 컨트롤 — 하나 고르기는 라디오, 여럿은 칸 없는 체크, 테두리만으로 충분하면 없음. |
| ⓔ Footer | 펼침 — 고른 상자 아래로 열리는 딸린 입력 · 안내. |
상자 하나가 선택지 하나다. 펼침을 뺀 상자 전체가 누르는 영역이고, 고른 상자는 테두리가 2px 짙은 색으로 바뀐다(바탕은 그대로). 누르면 바탕이 바뀌고 누르는 자리(콘텐츠 + 컨트롤)만 2px 거리로 준다(SEED scaleScope: content · 기초 Feedback v104).
부위
| 부위 | 설명 |
|---|---|
root | 상자 — 테두리 · 바탕 · 모서리. 펼침을 뺀 전부가 누르는 영역 |
trigger | 누르는 자리(label) — 콘텐츠 + 오른쪽 컨트롤. 누르면 이 자리만 준다 |
content | 콘텐츠 — 앞 · 본문 |
prefix | 앞 — 아이콘 22 · 그림 |
body | 본문 — 제목 + 설명 |
label | 제목 — 배지를 옆에 둘 수 있다 |
description | 설명 — 두 줄까지 |
control | 오른쪽 컨트롤 — Radiomark(하나 고르기) · 칸 없는 체크(여럿) · 없음 |
footer | 펼침 — 고른 상자 아래 딸린 입력 · 안내 |
focusRing | 키보드 포커스 링(웹) — 상자 바깥 |
group | 묶음 — 1 ~ 3열 격자(하나 고르기 radiogroup · 여럿 fieldset) |
Properties
Control
| 컨트롤 | 언제 |
|---|---|
라디오(RadioSelectBox) | 하나만 고른다 — 고른 것과 안 고른 것이 모두 보인다 |
칸 없는 체크(CheckSelectBox) | 여럿을 고른다 — 상자가 칸 노릇을 하므로 칸을 또 두지 않는다(Checkbox 의 Ghost 모양 — 꺼지면 옅은 체크). 일부 선택(indeterminate)은 없다 — 부모 · 자식 묶음은 Checkbox |
없음(control="none") | 테두리만으로 고른 것이 보이면 — 3열처럼 좁을 때, 시각적 소음을 줄이고 싶을 때. 하나 · 여럿 모두 쓴다. 여럿이면 몇 개까지 고를 수 있는지 미리 적는다 |
컨트롤은 늘 오른쪽이다(List 의 라디오 · 스위치 줄과 같은 자리). '없음' 이어도 라디오 · 체크는 화면 밖에 그대로 있어 키보드와 화면 읽기 프로그램이 쓴다.
Layout · Columns
1열로 쌓으면 가로형(앞 · 본문 · 컨트롤이 한 줄, 세로 가운데), 2 ~ 3열이면 세로형(앞이 위, 컨트롤은 위 오른쪽)이다. 열 수는 묶음이 정하고(columns), 상자마다 layout 으로 바꿀 수 있다. 2열 이상이면 상자 높이를 가장 긴 상자에 맞춘다 — 같은 줄뿐 아니라 묶음의 모든 줄이 같은 높이다(SEED 와 같다). 3열은 폭이 좁으니 정보를 줄인다 — 제목과 짧은 설명만, 컨트롤은 없음.
배치
| 속성 | horizontal (기본) | vertical |
|---|---|---|
| trigger 상하 여백 | spacing-x4 16px | spacing-x5 20px |
| trigger 좌우 여백 | — | spacing-x4 16px |
| trigger 왼쪽 여백 | spacing-x5 20px | — |
| trigger 오른쪽 여백 | spacing-x4 16px | — |
| trigger 세로 맞춤 | center | flex-start — 컨트롤은 위 오른쪽 |
| content 간격 | spacing-x3 12px — 앞 ↔ 본문 | spacing-x2_5 10px — 앞(위) ↔ 본문 |
| content 쌓는 방향 | row | column |
Density — 낮은 상자
세로형(2 ~ 3열)에는 낮은 꼴(density="compact")이 있다 — 위아래 20 → 12, 앞(위) ↔ 글 10 → 4. 좌우 16 · 앞 아이콘 22 · 글자 · 테두리 · 컨트롤 자리 · 열 · 줄 사이는 그대로다. 제목만 있는 상자는 94 → 72 로 낮아진다(위아래 + 앞 22 + 사이 + 제목 줄 22). 묶음이 정하고(RadioSelectBoxGroup · CheckSelectBoxGroup 의 density) 가로형에는 걸리지 않는다. 언제 쓰는지는 Guidelines 의 "낮은 상자는 선택지가 많을 때만"(2026-10-29 사용자 결정 17-1 Q5 = B).
밀도 — 세로형
| 속성 | default (기본) | compact |
|---|---|---|
| trigger 상하 여백 | spacing-x5 20px | spacing-x3 12px — 20 → 12 — 좌우 16 · 컨트롤 자리 · 글자 · 앞 22 는 그대로(2026-10-29 17-1 Q5 = B) |
| trigger 좌우 여백 | spacing-x4 16px | spacing-x4 16px |
| trigger 세로 맞춤 | flex-start — 컨트롤은 위 오른쪽 | flex-start — 컨트롤은 위 오른쪽 |
| content 간격 | spacing-x2_5 10px — 앞(위) ↔ 본문 | spacing-x1 4px — 앞(위) ↔ 본문 10 → 4 |
| content 쌓는 방향 | column | column |
모든 상자에 공통인 값(상태마다 바뀌는 값은 아래 State):
공통
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
root | 모서리 | radius-r3 12px | |
root | 배경 | transparent | |
root | 테두리 두께 | 1px | 상자 안쪽에 그린다(inset) — 고른 테두리 2px 도 안쪽에 덧그려 내용이 밀리지 않는다 |
root | 테두리 | color-stroke-neutral-weak #E5E8EF · 다크 #404757 | |
root | 터치 영역 | 44 × 44 | 펼침을 뺀 상자 전체 — 가장 작은 상자(가로형 · 제목만)도 54 |
root | 전환 대상 | background-color | 고른 테두리는 따로 — 아래 motion |
root | 전환 시간 | motion-duration-color-transition 150ms | |
root | 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | |
trigger | 간격 | spacing-x1_5 6px | 콘텐츠와 컨트롤 사이 |
trigger | 남는 자리 채움 | 1 | 상자가 같은 높이로 늘면 남는 자리까지 채운다 — 내용이 짧은 상자의 빈 아래쪽도 눌린다(SEED flex-grow: 1) |
trigger | 커서 | pointer | 누르는 자리에만 — 펼침은 누르는 영역이 아니다 |
trigger | 전환 대상 | scale | |
trigger | 전환 시간 | motion-duration-pressed-scale 150ms | |
trigger | 전환 곡선 | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | |
prefix | 크기 | 22px | |
prefix | 색 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
body | 간격 | spacing-x0_5 2px | |
body | 오른쪽 여백 | spacing-x1 4px | 본문과 컨트롤 사이(trigger 간격에 더한다) |
label | 글자 | text-t5 16px / 22px | |
label | 굵기 | 500 | SEED 와 같다 — 선택지를 비교하며 읽는 자리라 List 제목(400)보다 한 단계 굵다(사용자 결정) |
label | 글자·아이콘 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
label | 간격 | spacing-x1 4px | 제목 옆 배지 사이 |
description | 글자 | text-t3 13px / 400 / 18px | |
description | 글자·아이콘 | color-fg-neutral-muted #535866 · 다크 #B7BDCC | SEED 와 같다 — List 설명(fg-neutral-subtle)보다 한 단계 진하다. 두 줄까지 |
control | 크기 | 20px | 하나 고르기 = Radiomark medium(neutral) · 여럿 = Checkbox Ghost medium(체크 14) · 없음. 따로 줄지 않고 자기 포커스 링도 그리지 않는다(링은 상자가). 상자를 올리거나 누르면 라디오는 자기 누름 색이 된다(고른 라디오는 한 단계 짙게 — SEED Radiomark pressed), 칸 없는 체크는 바탕이 없다 |
footer | 좌우 여백 | spacing-x5 20px | SEED 예제 값 — 화면마다 갈리지 않게 컴포넌트가 정한다 |
footer | 아래 여백 | spacing-x4 16px | |
group | 줄 사이 | spacing-component-default 12px | SEED spacing-y.component-default |
group | 열 사이 | spacing-x3 12px | |
group | 칸 수 | 1 ~ 3 | 2열 이상은 세로형 · 상자 높이를 가장 긴 상자에 맞춘다(줄마다 같은 높이 — SEED grid-auto-rows 1fr). 칸이 비면 1열로 쌓거나 열 수를 맞춘다 |
State
상자는 고름 · 고르지 않음과, 그 위에 호버 · 포커스 · 누름 · 비활성이 곱해진다.
| 상태 | 모습 |
|---|---|
enabled | 테두리 1px stroke-neutral-weak — 고르면 2px stroke-neutral-contrast |
hovered | 웹. 누름과 같은 바탕 bg-layer-default-pressed(v106) |
focused | 웹. 키보드 포커스에만 상자 바깥 링 2px · 띄움 2px(v106 · SEED). 라디오 · 체크는 자기 링을 끈다 — 링이 두 겹이 되지 않는다 |
pressed | 호버와 같은 바탕 + 누르는 자리만 2px 거리 축소(v104). 컨트롤은 따로 줄지 않는다 — 라디오는 자기 누름 색이 되고(고른 라디오는 한 단계 짙게, SEED Radiomark pressed), 칸 없는 체크는 바탕이 없다 |
disabled | 제목 · 설명 · 앞 아이콘이 fg-disabled, 테두리는 1px 옅은 선(고른 채 막히면 2px 옅은 선). 불투명도로 흐리게 하지 않는다(v106) |
enabled
hovered
focused
pressed
disabled
고름
| 속성 | unselected (기본) | selected |
|---|---|---|
| 테두리 | — | color-stroke-neutral-contrast #1A1F2E · 다크 #F5F6FA — 고른 상자만 짙은 테두리 — 바탕은 바꾸지 않는다(사용자 결정, SEED). v113 |
| 테두리 두께 | — | 2px |
고른 채 막혔을 때
| selected | disabled 전용 색(v106) — 불투명도로 흐리게 하지 않는다 |
|---|---|
unselected | — |
selected | 테두리 color-stroke-neutral-weak #E5E8EF · 다크 #404757 — 고른 채 막히면 2px 옅은 선 |
상태마다 enabled 에서 바뀌는 값:
호버 hovered
| 속성 | 값 |
|---|---|
| 배경 | color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D |
포커스 focused
| 속성 | 값 | 비고 |
|---|---|---|
| 링 두께 | 2px | |
| 링 간격 | 2px | 상자 바깥 — SEED 와 같다(상자는 화면 폭이 아니라 링이 잘리지 않는다). 컨트롤은 자기 링을 끈다 — 링이 두 겹이 되지 않는다 |
| 링 색 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
누름 pressed
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
root | 배경 | color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D | |
trigger | 배율 | 2px 거리 축소 | 누르는 자리(콘텐츠 + 컨트롤)만 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px. 테두리 · 바탕은 그대로, 컨트롤은 따로 줄지 않는다 |
비활성 disabled
| 부위 | 속성 | 값 |
|---|---|---|
root | 테두리 | color-stroke-neutral-weak #E5E8EF · 다크 #404757 |
trigger | 커서 | not-allowed |
prefix | 색 | color-fg-disabled #8A91A0 · 다크 #838997 |
label | 글자·아이콘 | color-fg-disabled #8A91A0 · 다크 #838997 |
description | 글자·아이콘 | 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-d2 100ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | border-color | SEED 는 0.1s 를 바로 적었다 — groo d2 = 100ms |
누름 — 누르는 자리 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale | 콘텐츠 + 컨트롤만 2px 거리 축소 — 모션 줄이기면 하지 않는다 |
펼침 — 열 때 · 높이 | 400ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | height | SEED 도 토큰 없이 400ms. 높이는 grid-template-rows 0fr → 1fr 로 바꾼다(내용 높이를 재지 않는다) |
펼침 — 열 때 · 투명도 | motion-duration-d6 300ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | opacity | |
펼침 — 닫을 때 · 높이 | motion-duration-d6 300ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | height | grid-template-rows 1fr → 0fr |
펼침 — 닫을 때 · 투명도 | 400ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | opacity | 내용이 높이보다 먼저 사라지지 않게 투명도가 늦다. 모션 줄이기면 높이는 바로 바뀌고 투명도만 150ms(기초 Motion) |
Prefix
앞에는 아이콘 22(fg-neutral)를 둔다. 세로형에서는 위에 선다. 아이콘 대신 그림(미리보기 같은)을 둘 때는 상자 안에서 크기를 정해 쓴다 — 22 를 따르지 않는다.
Footer
고른 상자 아래로 딸린 입력 · 안내를 펼친다(footer) — "횟수 지정" 의 반복 횟수, "고정 시간" 의 부여 시간처럼 그 선택지를 고를 때만 필요한 칸이다. 고르지 않은 상자의 칸은 숨긴다(닫히면 보이지 않고 Tab 도 닿지 않는다). 펼침 안쪽 여백은 좌우 20 · 아래 16 이다. 고르지 않았을 때만 보이거나(footerVisibility="when-not-selected") 늘 보이게("always") 할 수도 있다.
Group
하나 고르기 묶음은 RadioSelectBoxGroup(radiogroup), 여럿은 CheckSelectBoxGroup(fieldset)이다. 둘 다 이름이 있어야 한다 — 묶음을 Field 로 감싸면 Field 의 라벨이 이름이 되고(aria-labelledby 를 Field 가 잇는다), 아니면 aria-label · aria-labelledby 를 준다. 오류는 상자를 바꾸지 않고 묶음 아래 글로 알린다(Checkbox 때 정한 규칙 — Field 의 꼬리).
Guidelines
상자 전체가 누르는 영역
상자 전체를 하나의 누르는 영역으로 둔다 — 안쪽 일부만 눌리게 짜지 않는다. 더 알릴 것은 설명이나 고른 뒤 열리는 펼침에 쓴다(SEED).
고르기만 — 반영은 버튼으로
Select Box 는 고르는 도구이고 그 자체로 무언가를 실행하지 않는다. 고른 값은 저장 · 확인 · 다음 · 내보내기 같은 버튼으로 반영한다(SEED, 사용자 결정). 누르는 순간 바뀌는 하나 고르기(테마처럼)는 List 의 라디오 줄로 — 앞에 미리보기를 둔다. 누르면 바로 무언가를 하는 자리("빠르게 맞추기" 같은)는 버튼이다.
권한을 주는 여럿 고르기도 같다 — HR 관리 > 역할 및 권한의 사용자 역할은 칸 없는 체크 상자(이름 + 역할 설명)를 고르고 아래 "저장" 한 번으로 반영한다(2026-10-15 마무리 10A — 아래 코드 "여럿 고르고 저장"). 폰 · 데스크톱이 같은 모양이고, 역할이 늘어도 상자를 쌓는다(검색 시트 · 팝오버를 두지 않는다). 누를 때마다 저장하면 권한이 실수 한 번에 바로 바뀌고, 빨리 두 번 누르면 앞 변경을 모르는 목록을 보낼 수 있다.
간결하게
제목은 짧게, 설명은 두 줄까지 — 선택지를 빠르게 견주게 한다. 선택지마다의 설명을 드롭다운 아래 한 문단에 몰아 쓰지 않는다(지금 HR 휴가 정책의 "N (고정 시간): …").
여러 열은 같은 높이
2열 이상이면 가장 긴 내용을 담은 상자에 높이를 맞춘다(SEED — 묶음의 모든 줄이 같은 높이). 열 수는 화면 폭에 맞춘다. 마지막 줄에 빈 칸이 생겨(5개를 2열에 두는 것처럼) 어색하면 1열로 쌓는 것을 권한다 — 설명이 짧은 2열처럼 빈 칸이 어색하지 않으면 그대로 둘 수 있다. 화면마다 고른다(2026-10-22 사용자 결정 4B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym).
낮은 상자는 선택지가 많을 때만
한 시트 · 한 단계에 상자가 많아 100% 글자에서 모두 한 화면에 보여야 할 때만 낮은 상자(density="compact")를 쓴다 — 자산 추가 1단계의 종류 아홉(계좌 여섯 · 카드 둘 · 투자 하나, 2열 · 앞 아이콘 · 컨트롤 없음)처럼 기본 높이면 마지막 상자가 화면 아래 흐림 속에 숨는 자리다. 선택지가 2 ~ 4개면 기본 높이로 둔다 — 낮출 까닭이 없고, 누르는 자리만 작아진다. 낮춰도 넘치는 자리(글자 200% · 작은 화면)는 본문 스크롤 + 끝 흐림 그대로다(Bottom Sheet 의 "90% 를 넘으면 페이지" — 보통은 들어맞는 시트가 큰 글자로 넘친 것). 한 묶음 안에서 기본 상자와 낮은 상자를 섞지 않는다.
| 자리 | 밀도 |
|---|---|
| 선택지 2 ~ 4개 · 설명이 붙는 상자 · 가로형(1열) | 기본 |
| 한 시트에 세로형 상자가 많아 기본 높이로는 마지막 상자가 화면 밖(자산 종류 아홉) | 낮은 상자 |
여럿 고르기의 최대 개수
컨트롤이 없는 상자로 여럿을 고르게 하면 몇 개까지 고를 수 있는지 미리 적는다. 다 고른 뒤 또 고르면 고르지 않고 Snackbar 로 알린다(SEED).
다른 컴포넌트와 나누기
| 이런 자리 | 컴포넌트 |
|---|---|
| 설명 · 그림 · 딸린 입력이 붙는 선택지를 비교해 고른다(버튼으로 반영) — 정보 양이 먼저, 개수와 상관없이 | Select Box |
| 하나를 켜고 끈다(설명이 붙어도) | Checkbox(저장 때 적용) · Switch(누르는 순간) |
| 화면 폭 목록에서 고른다 · 누르는 순간 바뀌는 고르기 | List 의 라디오 · 체크 줄 |
| 짧은 키워드로 거르거나 붙인다 | Chip |
| 한 줄 설명으로 줄일 수 있다 · 자리가 좁다 | Select |
| 누르면 바로 무언가를 한다 | Button |
선택지 개수 · 글 길이로 고르는 판단 순서는 Select 의 "고르는 컴포넌트 고르기" 한 곳이다 — 설명이 붙으면 개수와 상관없이 Select Box, 한 줄 설명으로 줄일 수 있으면 Select(2026-10-22 사용자 결정 1B · 2B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym). Chip 과는 이렇게 견준다(SEED).
| Select Box | Chip(고르기) | |
|---|---|---|
| 정보 밀도 | 설명 · 그림 · 딸린 입력까지 | 핵심 키워드 하나 |
| 공간 | 다른 칸과 폭 · 줄을 맞춘다 | 글 길이만큼 — 폭이 들쭉날쭉하다 |
| 확정 | 버튼으로 반영(저장 · 다음) | 폼이면 버튼, 필터 바면 바로 |
코드
레시피 recipes/shadcn/components/ui/select-box.tsx 를 쓴다 — RadioSelectBoxGroup · RadioSelectBox · CheckSelectBoxGroup · CheckSelectBox. 아래 미리보기는 스펙 값으로 그린 모습이다.
하나 고르기
import { RadioSelectBox, RadioSelectBoxGroup } from "@/components/ui/select-box"
<RadioSelectBoxGroup value={end} onValueChange={setEnd} aria-label="종료">
<RadioSelectBox value="none" label="무기한" description="중지할 때까지 계속 반복" />
<RadioSelectBox value="count" label="횟수 지정" description="정한 횟수만큼 반복" />
<RadioSelectBox value="date" label="종료일 지정" description="정한 날까지 반복" />
</RadioSelectBoxGroup>펼침
import { Input } from "@/components/ui/input"
<RadioSelectBox
value="count"
label="횟수 지정"
description="정한 횟수만큼 반복"
footer={<Input aria-label="반복 횟수" defaultValue={12} />}
/>여럿 고르기
import { CheckSelectBox, CheckSelectBoxGroup } from "@/components/ui/select-box"
<CheckSelectBoxGroup aria-label="휴가 권한">
<CheckSelectBox label="휴가 조회" description="본인 휴가 내역을 조회할 수 있는 권한입니다." defaultChecked />
<CheckSelectBox label="휴가 신청" description="OT, 경조 휴가 등 휴가를 신청할 수 있는 권한입니다." />
</CheckSelectBoxGroup>여러 열 · 앞 · 컨트롤 없음
import { Braces, FileText, Sheet } from "lucide-react"
import { RadioSelectBox, RadioSelectBoxGroup } from "@/components/ui/select-box"
<RadioSelectBoxGroup columns={3} defaultValue="csv" aria-label="파일 형식">
<RadioSelectBox value="csv" control="none" prefix={<FileText />} label="CSV" description="구글시트" />
<RadioSelectBox value="xlsx" control="none" prefix={<Sheet />} label="Excel" description="엑셀" />
<RadioSelectBox value="json" control="none" prefix={<Braces />} label="JSON" description="백업용" />
</RadioSelectBoxGroup>낮은 상자 — 자산 종류
import { CalendarDays, Coins, Landmark, Percent, PiggyBank, Wallet } from "lucide-react"
import { RadioSelectBox, RadioSelectBoxGroup } from "@/components/ui/select-box"
{/* 한 시트에 종류 아홉(계좌 여섯 · 카드 둘 · 투자 하나) — 묶음마다 낮은 상자로, 100% 글자에서 모두 보이게 */}
<RadioSelectBoxGroup columns={2} density="compact" value={kind} onValueChange={setKind} aria-label="계좌">
<RadioSelectBox value="checking" control="none" prefix={<Landmark />} label="입출금" />
<RadioSelectBox value="deposit" control="none" prefix={<PiggyBank />} label="예금" />
<RadioSelectBox value="savings" control="none" prefix={<CalendarDays />} label="적금" />
<RadioSelectBox value="overdraft" control="none" prefix={<Percent />} label="마이너스 통장" />
<RadioSelectBox value="cash" control="none" prefix={<Wallet />} label="현금" />
<RadioSelectBox value="loan" control="none" prefix={<Coins />} label="대출" />
</RadioSelectBoxGroup>비활성
<RadioSelectBoxGroup defaultValue="fixed" aria-label="가변 부여 여부">
<RadioSelectBox value="fixed" label="고정 시간" description="정책에 등록된 부여 시간을 사용합니다." disabled />
<RadioSelectBox value="flex" label="가변 시간" description="사용자 또는 관리자가 입력한 시간 값으로 부여합니다." disabled />
</RadioSelectBoxGroup>여럿 고르고 저장 — HR 역할
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Field } from "@/components/ui/field"
import { CheckSelectBox, CheckSelectBoxGroup } from "@/components/ui/select-box"
const roles = [
{ code: "ADMIN", name: "관리자", description: "관리자 권한으로 시스템 운영에 필요한 전체 권한이 부여되어 있습니다." },
{ code: "USER", name: "일반 사용자", description: "일반 사용자 권한으로 시스템 관리에 필요한 권한을 제외한 일반적인 읽기, 쓰기 권한이 부여되어 있습니다." },
{ code: "APPROVER", name: "승인권자", description: "휴가 신청 항목에 대해 승인/반려를 할 수 있는 권한이 부여되어 있습니다." },
]
const saveUserRoles = (codes: string[]) => Promise.resolve(codes) // 사용자 역할 저장 요청 — 성공하면 토스트 "역할을 저장했어요"
const [saved, setSaved] = useState<string[]>(["USER", "APPROVER"])
const [picked, setPicked] = useState<string[]>(saved)
const added = picked.filter((c) => !saved.includes(c)).length
const removed = saved.filter((c) => !picked.includes(c)).length
const toggle = (code: string, on: boolean) =>
setPicked((list) => (on ? [...list, code] : list.filter((c) => c !== code)))
<Field label="역할" description="여럿 고를 수 있어요. 저장을 눌러야 바뀌어요.">
<CheckSelectBoxGroup>
{roles.map((r) => (
<CheckSelectBox
key={r.code}
label={r.name}
description={r.description}
checked={picked.includes(r.code)}
onCheckedChange={(on) => toggle(r.code, on)}
/>
))}
</CheckSelectBoxGroup>
</Field>
<div className="flex items-center justify-end gap-x3">
<span className="text-t4 text-fg-neutral-subtle">
{added + removed > 0 ? `더함 ${added} · 뺌 ${removed} — 아직 저장 안 됨` : "바뀐 것 없음"}
</span>
{/* 바뀐 것이 없으면 막는다 — 저장은 한 번, 누를 때마다 저장하지 않는다 */}
<Button disabled={added + removed === 0} onClick={() => saveUserRoles(picked).then(() => setSaved(picked))}>
저장
</Button>
</div>Behavior
| 인터랙션 | 동작 |
|---|---|
| Click / Tap | 펼침을 뺀 상자 어디를 눌러도 고른다(하나 고르기는 바뀌고, 여럿은 켜고 끈다). 펼침 안의 입력은 그 입력만. disabled 면 무시. |
| Hover(웹) | 누름과 같은 바탕. |
Keyboard Tab | 하나 고르기 묶음은 고른 상자 하나로 들어온다 · 여럿은 상자마다. |
Keyboard ↑ ↓ ← → | 하나 고르기 묶음 안에서 옮기며 고른다(Radix). |
Keyboard Home · End | 하나 고르기 묶음은 받지 않는다 — 초점도 고른 상자도 그대로, 화면도 스크롤되지 않는다(PageUp · PageDown 도). 첫 · 끝 상자로는 화살표로 간다 — SEED · 진짜 라디오 · WAI-ARIA APG Radio Group 과 같다(2026-10-17 사용자 결정 — 03-E 결정 3 A). 펼침 안의 입력칸에서는 글 처음 · 끝으로 가는 그대로다. |
Keyboard Space | 고른다 · 켜고 끈다. 누르고 있는 동안은 누름 모습이다. |
Keyboard Enter | 폼을 제출한다 — 진짜 input 처럼 폼의 기본 버튼을 누른다. 고르지 않는다(Checkbox · Radio 와 같다, 2026-10-08). |
| 반영 | 저장 · 다음 같은 버튼이 한다 — 상자를 고르는 것만으로 반영하지 않는다. |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 제목 fg-neutral 10.32:1 이상, 설명 fg-neutral-muted — 흰 바탕 7.11 · 7.70(라이트 · 다크), 누름 바탕 6.70 · 5.93, 시트 바탕 7.11 · 6.67 ✓. 막힌 상자의 fg-disabled 는 기준 밖(비활성 UI) |
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 고른 테두리 stroke-neutral-contrast 16.41 · 13.42, 누름 바탕 위 15.48 · 10.32 ✓. 고르지 않은 테두리(1.23 · 1.56)는 상태를 알리지 않는 틀이다 — 고름은 2px 짙은 테두리와 라디오 · 체크가 알린다 |
| WCAG 1.4.1 Use of color | 고름은 색과 함께 테두리 두께(1 → 2px) · 라디오 · 체크로도 알린다 |
| WCAG 2.4.7 Focus visible | 키보드 포커스에만 상자 바깥 링 2px · 띄움 2px(stroke-focus-ring) |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 가장 작은 상자(가로형 · 제목만) 54 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 펼침을 뺀 상자 전체 — 54 ✓ |
| ARIA | 하나 고르기 묶음은 role="radiogroup", 여럿은 <fieldset> — 둘 다 이름(aria-label · aria-labelledby) 필수. 상자의 라디오 · 체크가 <label> 에 묶여 제목 + 설명이 이름이다. 컨트롤이 '없음' 이어도 라디오 · 체크는 화면 밖에 남는다 |
| Reduced motion | 모션 줄이기면 누르는 자리의 축소를 뺀다. 펼침은 높이가 바로 바뀌고 투명도만 150ms(기초 Motion) |
Do / Don't
✅ Do
- 설명 · 그림 · 딸린 입력이 붙는 선택지를 Select Box 로 견주게 한다.
- 고른 값은 저장 · 다음 같은 버튼으로 반영한다.
- 고를 때만 필요한 칸은 그 상자의 펼침에 둔다.
- 여러 열이면 상자 높이를 가장 긴 상자에 맞춘다.
- 한 시트에 세로형 상자가 많아 마지막 상자가 화면 밖이면 낮은 상자(
density="compact")로 모두 보이게 한다.
❌ Don't
- 상자를 눌러 바로 무언가를 실행하기.
- 상자 안 일부(체크박스 · 글자)만 눌리게 짜기.
- 고른 상자를 바탕 색만으로 알리기.
- 선택지마다의 설명을 아래 한 문단에 몰아 쓰기.
- 선택지 2 ~ 4개에 낮은 상자 · 한 묶음에서 기본 상자와 낮은 상자 섞기 · 위아래를 12 보다 더 줄이기.
Specification
select-box.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Select Box 를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 모서리radius | radius-r312px |
| 배경background | transparent | ||
| 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757 | ||
| 테두리 두께borderWidth | 1px상자 안쪽에 그린다(inset) — 고른 테두리 2px 도 안쪽에 덧그려 내용이 밀리지 않는다 | ||
| 터치 영역touchTarget | 44 × 44펼침을 뺀 상자 전체 — 가장 작은 상자(가로형 · 제목만)도 54 | ||
| 전환 대상transitionProperty | background-color고른 테두리는 따로 — 아래 motion | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
trigger | enabled | 간격gap | spacing-x1_56px콘텐츠와 컨트롤 사이 |
| 남는 자리 채움grow | 1상자가 같은 높이로 늘면 남는 자리까지 채운다 — 내용이 짧은 상자의 빈 아래쪽도 눌린다(SEED flex-grow: 1) | ||
| 커서cursor | pointer누르는 자리에만 — 펼침은 누르는 영역이 아니다 | ||
| 전환 대상transitionProperty | scale | ||
| 전환 시간transitionDuration | motion-duration-pressed-scale150ms | ||
| 전환 곡선transitionEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
prefix | enabled | 크기size | 22px |
| 색color | fg-neutral#1A1F2E다크 #F5F6FA | ||
body | enabled | 오른쪽 여백paddingRight | spacing-x14px본문과 컨트롤 사이(trigger 간격에 더한다) |
| 간격gap | spacing-x0_52px | ||
label | enabled | 간격gap | spacing-x14px제목 옆 배지 사이 |
| 글자·아이콘foreground | fg-neutral#1A1F2E다크 #F5F6FA | ||
| 글자typography | text-t516px / 22px | ||
| 굵기fontWeight | 500SEED 와 같다 — 선택지를 비교하며 읽는 자리라 List 제목(400)보다 한 단계 굵다(사용자 결정) | ||
description | enabled | 글자·아이콘foreground | fg-neutral-muted#535866다크 #B7BDCCSEED 와 같다 — List 설명(fg-neutral-subtle)보다 한 단계 진하다. 두 줄까지 |
| 글자typography | text-t313px / 18px / 400 | ||
control | enabled | 크기size | 20px하나 고르기 = Radiomark medium(neutral) · 여럿 = Checkbox Ghost medium(체크 14) · 없음. 따로 줄지 않고 자기 포커스 링도 그리지 않는다(링은 상자가). 상자를 올리거나 누르면 라디오는 자기 누름 색이 된다(고른 라디오는 한 단계 짙게 — SEED Radiomark pressed), 칸 없는 체크는 바탕이 없다 |
footer | enabled | 좌우 여백paddingX | spacing-x520pxSEED 예제 값 — 화면마다 갈리지 않게 컴포넌트가 정한다 |
| 아래 여백paddingBottom | spacing-x416px | ||
group | enabled | 칸 수columns | 1 ~ 32열 이상은 세로형 · 상자 높이를 가장 긴 상자에 맞춘다(줄마다 같은 높이 — SEED grid-auto-rows 1fr). 칸이 비면 1열로 쌓거나 열 수를 맞춘다 |
| 열 사이gapX | spacing-x312px | ||
| 줄 사이gapY | spacing-component-default12pxSEED spacing-y.component-default | ||
root | hovered | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
focusRing | focused | 링 두께width | 2px |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | 2px상자 바깥 — SEED 와 같다(상자는 화면 폭이 아니라 링이 잘리지 않는다). 컨트롤은 자기 링을 끈다 — 링이 두 겹이 되지 않는다 | ||
root | pressed | 배경background | bg-layer-default-pressed#F7F8FD다크 #353B4D |
trigger | pressed | 배율scale | 2px 거리 축소누르는 자리(콘텐츠 + 컨트롤)만 — 기준 길이 max(높이, 폭 ÷ 4, 24) 에서 2px. 테두리 · 바탕은 그대로, 컨트롤은 따로 줄지 않는다 |
root | disabled | 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757 |
trigger | disabled | 커서cursor | not-allowed |
prefix | disabled | 색color | fg-disabled#8A91A0다크 #838997 |
label | disabled | 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997 |
description | disabled | 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997 |
layout=horizontal
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 상하 여백paddingY | spacing-x416px |
| 왼쪽 여백paddingLeft | spacing-x520px | ||
| 오른쪽 여백paddingRight | spacing-x416px | ||
| 세로 맞춤alignItems | center | ||
content | enabled | 간격gap | spacing-x312px앞 ↔ 본문 |
| 쌓는 방향direction | row |
layout=vertical
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 상하 여백paddingY | spacing-x520px |
| 좌우 여백paddingX | spacing-x416px | ||
| 세로 맞춤alignItems | flex-start컨트롤은 위 오른쪽 | ||
content | enabled | 간격gap | spacing-x2_510px앞(위) ↔ 본문 |
| 쌓는 방향direction | column |
layout=vertical, density=compact
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
trigger | enabled | 상하 여백paddingY | spacing-x312px20 → 12 — 좌우 16 · 컨트롤 자리 · 글자 · 앞 22 는 그대로(2026-10-29 17-1 Q5 = B) |
content | enabled | 간격gap | spacing-x14px앞(위) ↔ 본문 10 → 4 |
selected=selected
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 테두리borderColor | stroke-neutral-contrast#1A1F2E다크 #F5F6FA고른 상자만 짙은 테두리 — 바탕은 바꾸지 않는다(사용자 결정, SEED). v113 |
| 테두리 두께borderWidth | 2px | ||
root | disabled | 테두리borderColor | stroke-neutral-weak#E5E8EF다크 #404757고른 채 막히면 2px 옅은 선 |
SEED 와 다른 점
- 누름 · 호버 바탕은 불투명한
bg-layer-default-pressed— SEED 의 transparent-pressed 는 투명도가 있어 검사기가 받지 않는다(v102). - 웹의 호버 · 포커스 상태를 둔다(v106) — 호버는 누름과 같은 바탕.
- 펼침 안쪽 여백을 컴포넌트가 정한다(좌우 20 · 아래 16) — SEED 는 쓰는 쪽이 정한다(예제 값이 이것이다).
- 여럿 고르기의 체크는 Checkbox 의 Ghost 칸(체크 14) — SEED 는 선택 상자 전용 체크(15)이고 호버에 색이 한 단계 진해진다. groo 는 호버에 색을 바꾸지 않는다 — 정할 때
fg-placeholder와fg-neutral-subtle이 같은 색이었다. v119(2026-10-16 글자 역할 D7)에서fg-placeholder가 SEED 처럼 gray-600 으로 옅어져 두 색은 이제 갈리지만, 호버 색은 이 결정 밖이라 그대로 둔다. - 빈 칸은 권장으로 — 5개를 2열에 두어 빈 칸이 어색하면 1열로 쌓기를 권한다(제품 조사에서 더했다 — 2026-10-22 4B 로 금지에서 권장으로). SEED 는 열을 화면 폭에 맞추고 높이만 통일하라고 한다.
- 묶음의 칸 이름 · 설명 · 오류는 묶음이 아니라 Field 가 그린다(2026-10-01 — SEED 의 Select Box Group Field 자리). SEED 의 Fieldset 은 따로 두지 않는다.
- 고른 테두리
stroke-neutral-contrast는 v113 에 더한 역할이다(SEED 와 같은 gray-1000).
Migration notes
2026-10-29 — 세로형 낮은 상자(density compact)
사용자 결정 17-1 Q5 = B — 자산 추가 1단계 "무엇을 추가할까요?" 의 종류 아홉(2열 세로형 · 앞 아이콘 · 컨트롤 없음)이 폰 시트를 넘어 마지막 "투자" 상자가 화면 아래 흐림 속에 숨었다. 세로형 상자의 위아래를 20 → 12, 앞 ↔ 글을 10 → 4 로 낮춰 100% 글자에서 아홉이 시트 한 화면에 다 보이게 한다. 한 화면만의 값으로 두지 않고 축 density(default · compact)로 스펙에 올렸다 — 세로형에만 걸리고, 선택지가 많을 때만 쓴다. 고르지 않은 안: A(지금 그대로 — 스크롤 + 아래 흐림, 투자를 찾지 못할 수 있다) · C(1열 목록 + 라디오 — 상자 모양 결정을 바꾸고 웹 390 에서 여전히 넘친다) · D(시트 대신 페이지 — 2단계 폼까지 흐름이 바뀐다). 글자 200% 에서는 어느 안도 한 화면에 들지 않아 본문 스크롤 + 끝 흐림 그대로다. 설명 https://claude.ai/artifact/2wHgeNPdst2q6LQzdD1AvS. 레시피 묶음에 density 를, 미리보기에 .psb--compact 를, 사이트에 그림 density · ex-compact 를 더했다. 새 토큰 없음.
2026-10-22 — 개수는 판단 순서 하나 · 홀수 2열은 권장
"2 ~ 6개 / 7개 이상" 을 걷고 "정보 양이 먼저 — 설명이 붙으면 개수와 상관없이 Select Box" 로, 개수 판단은 Select 의 "고르는 컴포넌트 고르기" 링크로(2B · 1B). Select Box · Chip 견주기 표(SEED)를 더했다. "홀수 개를 2열에 두지 않는다" 는 10-01 페이지에 보였지만 묻지 않은 것이라 Don't 에서 빼고 배치 절의 권장 한 줄로(4B). "토스트 같은 안내" 를 Snackbar 로 — 사용자 결정 https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym. 값 변경 없음.
2026-10-17 — 하나 고르기 묶음은 Home · End 를 받지 않는다(03-E 결정 3 A)
웹 공용 라디오 묶음(Chip 하나 고르기 · Radio Group · Segmented Control · Select Box 하나 고르기 · List 라디오)은 Radix RovingFocusGroup 이 Home · End(· PageUp · PageDown)로 초점만 첫 · 끝으로 옮겨 초점과 고른 것이 갈렸다. 사용자가 비교 페이지에서 "받지 않는다"(A — 화살표만, SEED · 진짜 라디오 · APG)를 골랐다. 레시피는 묶음이 캡처 단계에서 라디오의 그 키를 막는다(ignoreRadioJumpKeys — radio-group.tsx, Select Box 하나 고르기 도 쓴다). 웹 ds 의 같은 묶음도 같게, 앱은 바뀌는 것 없음. 옮기며 고르는 안(B — Tabs 처럼)은 고르지 않았다. 옛 스펙은 select-box.history/v-pre-03e-03b.md.
2026-10-15 — HR 역할 고르기는 여럿 고르기 상자 + 저장
사용자가 마무리 비교 페이지 10A 에서 정했다 — HR 관리 > 역할 및 권한 > 사용자 탭의 역할 고르기는 CheckSelectBox(이름 t5 500 + 역할 설명 t3) 여럿 + 아래 "저장" 한 번(바뀐 것이 없으면 막힘), 폰 · 데스크톱 같은 모양이다. 데스크톱 팝오버 · 폰 시트 두 벌을 걷는다 — Command(cmdk)를 디자인 시스템과 제품에서 걷는 일의 하나다. List 체크 줄 · 누르는 순간 저장(10B) · Select 여럿 고르기(10C) · 검색 시트 · 팝오버 + 완료(10D)는 고르지 않았다.
- 데스크톱 — 팝오버 250 안 cmdk 검색 + 체크 줄이고, 줄을 누를 때마다
await없이 사용자 전체를 PUT 한다(hr-frontfeatures/admin-authority/ui/UserRoleAssignment.tsx:29-36 · 98-132· 저장UserManagementPanel.tsx:60-87). 두 번 빨리 누르면 두 번째 요청이 첫 변경을 모르는 목록으로 계산될 수 있다(코드로 읽은 위험). 검색은 역할 이름으로만 된다. - 폰 — 시트 85vh + 검색칸(이름 · 코드) + 체크 카드(
div onClick— 키보드로 못 닿는다) + "완료" 로 한 번 저장(MobileRoleSelector.tsx:46-64 · 80-140 · 140-145). 저장 오류를 스스로 삼켜 실패해도 시트가 닫힌다. - 역할 수 — 관리자가 "역할" 탭에서 늘리고 지울 수 있고 상한이 없다. 기본은 넷(groo-sql
hr/data/4. roles.sql— 관리자 · 일반 사용자 · 승인권자 · 업무 코드 관리자). 비교 페이지는 여덟 개로 그렸다 — 가장 긴 "일반 사용자" 설명도 폰 상자에서 두 줄에 든다. 설명은 1000자까지 쓸 수 있어(Role.java:52) 더 길면 두 줄을 넘는다. - 걷는 것 —
shared/ui/shadcn/command.tsx· 배럴 재수출(shared/ui/shadcn/index.ts:39-49) ·cmdk의존, 고아가 되는 번역 키authority.noRoleFound. 같은 시트 모양의 "권한 관리"(MobilePermissionDrawer.tsx— 역할 탭)는 그 차례에 이 모양으로 맞춘다.
2026-10-08 — Enter 는 폼 제출
Checkbox · Radio 의 Enter 가 폼을 제출하게 됐다(사용자 결정 — 비교 페이지 B1, 진짜 input · SEED 와 같다). 여럿 고르기 상자는 Checkmark 를 써서 그대로 따라오고, 하나 고르기 묶음(RadioSelectBoxGroup)은 Radio 와 같은 처리(submitOnRadioEnter)로 Enter 를 받는다. 고르기는 그대로 Space · 화살표다.
2026-10-01 — SEED Select Box 로
사용자가 비교 페이지에서 정했다 — Select Box 를 새로 두고 Tile 을 걷는다 · 고른 상자는 짙은 테두리 2px(새 역할 stroke-neutral-contrast, v113) · 바탕은 그대로, 브랜드 톤 없음 · 컨트롤은 라디오 · 칸 없는 체크 · 없음 · 글자 16/500 · 설명 13 진한 회색 · 고르기만 하고 반영은 버튼으로(테마는 List 라디오 줄).
Tile 을 걷는다 — 테마 미리보기 카드는 Select Box(앞 그림) 또는 누르는 순간 바뀌면 List 라디오 줄(앞 미리보기)로 그린다. 옛 스펙은 tile.history/ 에 남겼다.
| 옛 Tile | 새 Select Box |
|---|---|
| 미선택은 1px 옅은 테두리(제품은 투명) · 고르면 1.5px 브랜드 + 옅은 브랜드 바탕 | 미선택 1px stroke-neutral-weak · 고르면 2px stroke-neutral-contrast(안쪽 덧그림 — 내용이 밀리지 않는다), 바탕은 그대로 |
| 고른 것에만 오른쪽 위 체크 16(브랜드) | 오른쪽 라디오 20 · 칸 없는 체크 · 없음 — 고른 것과 안 고른 것이 모두 보인다 |
| 제목 14 · semi, 설명 11 ~ 12 | 제목 16 · 500, 설명 13 fg-neutral-muted |
| 미리보기 40 고정 · 하나 고르기만 | 앞 아이콘 22 · 그림, 하나 · 여럿 고르기, 1 ~ 3열, 펼침 |
| 누르는 순간 적용(테마) | 버튼으로 반영 — 테마처럼 누르는 순간 바뀌는 고르기는 List 라디오 줄 |
제품은 앱 적용 단계에서 옮긴다(2026-10-01 조사). 상자 모양 고르기를 그리는 공용 부품이 사실상 없어 화면마다 손으로 짰다.
- Desk 웹 — 패턴 6(화면 7곳): 테마(공용 Tile) · 내보내기 기간 · 파일 형식 · 더치페이 분배 방식 · 필터 거래 종류 · 반복 거래 종료(두 화면). 선택 표시가 다섯 가지이고, 넷이 고른 상태를 보조기술에 알리지 않는다(맨
<button>). 분배 방식은 고른 것과 안 고른 것의 바탕 대비가 1.02:1 이라 사실상 제목 색으로만 갈린다. 종료는 입력칸이role=radio안에 있고 radiogroup 이 없다(화살표 · 묶음 이름 없음, 입력칸이 고르기 전에도 보인다). 테마 Tile 은 스펙과 어긋난다 — 미선택이 투명이라 고른 것만 상자로 보이고, 고르면 1 → 1.5px 로 내용이 흔들리고, 제목의text-body클래스는 CSS 가 생기지 않는다. 내보내기 대화상자(ExportDialog)는 쓰는 곳이 없는 죽은 코드다. 가져오기 출처 드롭다운은 항목마다 '이름 · 설명' 을 한 줄로 이어 붙였다(Select Box 감). - Desk 앱 — 상자 6곳 + 후보 8(반복 종료 · 가져오기 출처 · 거래 옮기기 · 고정 금액 · 계좌 종류 · 결제 주기 · 참여자 · 금액 가리기). 선택 모양 다섯 가지, 선택 상태가 Semantics 에 없다. 누르는 영역이 44 보다 작은 곳(필터 거래 종류 41.5 · 금액 가리기 31.5 · 계좌 종류 28)이 있고, 불투명 바탕이 물결을 가린다. PTile 은 tile.yaml 과도 다르다. 구독 Pro 카드는 누를 수 없는데 2px 브랜드 테두리라 고른 상자처럼 보인다. "빠르게 맞추기" 는 상자 모양이지만 누르면 바로 금액을 고치는 실행이다(→ 버튼).
- HR 웹 — 상자 4 패턴 · 5곳이 모두 손으로 짠 여럿 고르기(권한 · 역할 할당 · 플랜 정책 · 시스템 데일리 체크)이고, 셋은 고르면 체크박스만 바뀌고 상자는 그대로다(체크박스 테두리 #eee 는 흰 바탕 1.16:1). 상자 전체를 누르는 셋은
div onClick이라 키보드로 못 쓰고, 시스템 데일리 체크는 보조기술도 못 쓴다. 하나 고르기 상자는 없다 — 하나 고르기는 전부 드롭다운이다. Select Box 후보 22(강 7): 휴가 정책의 부여 방법 · 가변 부여 여부(고정이면 부여 시간 펼침) · 반복 여부(1회성이면 최대 부여 횟수 펼침) · 분단위 부여 여부, 휴가 신청의 정책, 휴가 부여의 부여일 · 만료일, 공휴일의 음력 여부. 지금은 옵션 설명을 드롭다운 아래 한 문단에 몰아 쓰고("N (고정 시간): …"), Y/N 코드값을 그대로 보인다. - 경계 — 프리셋 "고정 금액 사용"(켜면 금액 칸)과 자산 "전체 자산 합계에 포함" 은 선택지가 하나라 Checkbox 다(설명 · 딸린 입력이 붙어도). 더치페이 참여자 고르기는 List 의 체크 줄(아바타 앞)이다.
Segmented Control
같은 내용을 2 ~ 4가지로 바로 거르거나 · 정렬하거나 · 다르게 보는 컨트롤. 그 내용 바로 위에, 한 화면에 하나 둔다. 다른 구역으로 옮기면 Tabs, 2 ~ 4개 짧은 폼 값은 Chip, 목록 조건을 여럿 걸고 풀면 Chip 의 필터 바다.
Select
짧은 선택지 5개 이상에서 폼에 넣을 값을 고르는 칸. 결제 수단 · 통화 · 휴가 정책 · 반복 단위처럼 한 줄 글(+ 한 줄 설명)로 충분한 선택지를 칸 아래 목록으로 연다. 달력 · 시각 · 아이콘 격자 · 검색이 필요한 긴 목록은 Input Button, 설명 · 그림이 붙는 2 ~ 6개는 Select Box다.