Wheel Picker
순서가 있는 값을 세로로 굴려 고르는 휠 — 한 칼럼 이상. Time Picker 의 바탕이고, Date Picker 머리의 연 · 월 휠, 달만 고르는 자리(예산 · 홈 · 카드 실적의 달)에 쓴다. 적은 선택지는 Radio · Segmented Control, 긴 목록은 Select, 정해진 목록이 아닌 값은 Input 이다.
구조는 당근 SEED Wheel Picker(Apache-2.0)를 따른다 — 칼럼 · 항목 · 가운데 선택 띠 · 위아래 안개, 크기 small 36 · medium 44. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-03 사용자 결정). groo 에 처음 두는 컴포넌트다.
수치 원본은 wheel-picker.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다.
직접 골라 보기
크기 · 보이는 칸 수 · 칼럼을 고르면 스펙대로 그린 휠이 바뀐다. 실제로 굴리고 누를 수 있다.
고른 값: 2026년 10월 — 값은 휠이 멈춘 뒤 한 번만 바뀌어요.
import { WheelPicker, WheelPickerColumn } from "@/components/ui/wheel-picker"
<WheelPicker aria-label="월 선택">
<WheelPickerColumn aria-label="연도" options={years} align="right" className="w-[120px]" value={year} onValueChange={setYear} />
<WheelPickerColumn aria-label="월" options={months} align="left" className="w-[96px]" loop value={month} onValueChange={setMonth} />
</WheelPicker>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Column | 칼럼 — 항목 글 폭만큼. 칼럼 묶음은 휠 가운데. |
| ⓑ Item | 항목 — 짧은 한 줄 글. |
| ⓒ Indicator | 선택 띠 — 가운데 줄. 띠에 걸친 글자만 짙게 칠한다. |
| ⓓ Fog | 안개 — 위아래 끝이 바탕으로 흐려진다. |
부위
| 부위 | 설명 |
|---|---|
root | 휠 — 칼럼 묶음을 가운데에 모은다 |
column | 칼럼 — 항목 글 폭만큼(좌우 여백 포함). 칼럼마다 Tab 자리 하나 |
item | 항목 — 짧은 한 줄 글(숫자 · 짧은 낱말). 버튼 · 링크 · 여러 줄은 넣지 않는다 |
indicator | 선택 띠 — 가운데 줄, 칼럼을 모두 가로지른다 |
fog | 안개 — 위아래 끝이 바탕으로 흐려진다 |
focusRing | 키보드 포커스 링(웹) |
Properties
Size
medium(44 · 글자 26)이 기본이다 — 시각 · 월 고르기 · 연 · 월 휠. small(36 · 글자 20)은 좁은 팝오버에만 쓴다. 글자는 글자 크기 설정을 따르지 않는 px 이다 — 휠 칸이 넘치지 않게.
small 은 좁은 팝오버에만 쓴다 — 누르는 높이 36 은 AAA(44)에 못 미친다. 달력 머리의 연 · 월 휠은 medium · 7칸(308 · 안개 123)이다.
크기
| size | item 높이 | item 글자 | indicator 높이 | 설명 |
|---|---|---|---|---|
small | 36px | text-t7-static 20px / 27px — 20 / 27 — 글자 크기 설정을 따르지 않는 px(웹 text-t7-static, 앱 TextScaler.noScaling — SEED t7-static) | 36px | 36 — 좁은 자리(작은 팝오버) |
medium (기본) | 44px | text-t10-static 26px / 35px — 26 / 35 — 글자 크기 설정을 따르지 않는 px(웹 text-t10-static, 앱 TextScaler.noScaling — SEED t10-static), 휠 칸이 넘치지 않게 | 44px | 44 — 기본(시각 · 월 고르기 · 연 · 월 휠) |
보이는 칸 수
기본 5칸(220), 달력 머리의 연 · 월 휠은 요일 줄과 6주 자리를 덮는 7칸(308)이다. 홀수만 쓴다 — 가운데 줄이 있어야 한다. 안개는 휠 높이의 40% 와 항목 3칸 중 짧은 쪽이다(5칸 88 · 7칸 123).
공통
| 부위 | 속성 | 값 | 비고 |
|---|---|---|---|
root | 높이 | 항목 높이 × 보이는 수 | medium 5개 220 · 7개 308, small 5개 180 |
root | 배경 | color-bg-layer-floating #FFFFFF · 다크 #2D3346 | |
column | 너비 | 항목 글 폭 + 좌우 16 | 칼럼 묶음은 휠 가운데. 반복(loop)은 칼럼마다 — 끄면 첫 값 · 끝 값에서 멈춘다. 연 · 월 휠(달력 머리 · 달만 고르기)은 연 120 · 월 96 고정(date-picker.yaml) |
item | 좌우 여백 | spacing-x4 16px | |
item | 굵기 | 500 | |
item | 글자·아이콘 | color-fg-disabled #8A91A0 · 다크 #838997 | 가운데가 아닌 항목 — 띠에 걸친 부분만 아래 고른 색으로 칠한다(굴리는 동안에도) |
item | 고른 글자색 | color-fg-neutral #1A1F2E · 다크 #F5F6FA | |
item | 숫자 폭 | tabular-nums | 숫자 폭을 같게 |
indicator | 띠 들임 | spacing-x4 16px | 휠 좌우 끝에서 16 들인다 |
indicator | 모서리 | radius-r2 8px | |
indicator | 배경 | color-bg-neutral-weak #F5F6FA · 다크 #353B4D | |
fog | 높이 | min(휠 높이 × 40%, 항목 3칸) | medium 5개 88 · 7개 123 · small 5개 72 — 위아래 끝에서 바탕색으로(gradient-fade-mask) |
상태
| 상태 | item 글자·아이콘 | item 고른 글자색 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|
enabled 기본 | color-fg-disabled #8A91A0 · 다크 #838997 — 가운데가 아닌 항목 — 띠에 걸친 부분만 아래 고른 색으로 칠한다(굴리는 동안에도) | color-fg-neutral #1A1F2E · 다크 #F5F6FA | — | — | — |
focused 웹 — 키보드로 칼럼에 들어오면 고른 항목 둘레에 링 | color-fg-disabled #8A91A0 · 다크 #838997 — 가운데가 아닌 항목 — 띠에 걸친 부분만 아래 고른 색으로 칠한다(굴리는 동안에도) | color-fg-neutral #1A1F2E · 다크 #F5F6FA | 2px | -2px — 가운데(고른) 항목 둘레 안쪽, 모서리 8 — 키보드로 들어올 때만. 터치 · 마우스로 굴리는 동안은 숨긴다 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
disabled 막힌 휠 — 값은 그대로, 굴릴 수도 초점이 갈 수도 없다 | color-fg-disabled #8A91A0 · 다크 #838997 — 가운데가 아닌 항목 — 띠에 걸친 부분만 아래 고른 색으로 칠한다(굴리는 동안에도) | color-fg-disabled #8A91A0 · 다크 #838997 — 고른 항목도 흐리게 — 띠는 남는다 | — | — | — |
Guidelines
휠을 쓰는 자리
순서가 있고 앞뒤 값을 보며 고르는 값 — 시각 · 연 · 월. 하루를 고르면 달력(Date Picker)이 기본이다 — 요일 · 오늘과의 거리가 보인다. 몇 해 앞뒤의 먼 날도 달력 제목을 눌러 연 · 월 휠로 옮긴 뒤 날을 누른다. 연 · 월 · 일 세 휠은 피한다 — 31일이 없는 달을 따로 막아야 하고, 요일 · 오늘과의 거리가 보이지 않는다(2026-10-22 사용자 결정 3B — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L). 순서가 없거나 고를 것이 적으면 다른 컴포넌트다 — Select 의 "고르는 컴포넌트 고르기" 판단 순서로 고른다(2026-10-22 사용자 결정 2B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym).
달만 고르기
예산 · 홈 · 카드 실적처럼 달만 고르는 자리는 연 | 월 두 칼럼 휠을 시트(1280 미만) · 팝오버(이상)로 열고 "완료" 로 넣는다 — 달력의 연 · 월 휠과 같은 모양이다(연 120 오른쪽 · 월 96 왼쪽 정렬, 보이는 칸 5). 4 × 3 월 격자는 두지 않는다. 시트 · 팝오버의 제목은 "월 선택" 이다 — 캘린더 · 예산 어디서 열어도 같은 말이고 휠의 이름(aria-label)과 같다(2026-10-29 I-27 — 웹 · 앱 같게).
항목
짧은 한 줄 글 — 숫자 · 짧은 낱말. 시 · 분처럼 숫자만으로 읽히는 칼럼은 단위를 칼럼 이름으로 알리고 항목마다 붙이지 않는다. 연 · 월은 날짜 표기 그대로 "2026년" · "10월" 이다(달력의 연 · 월 휠과 같다). 버튼 · 링크 · 여러 줄 글은 넣지 않는다. 아이콘을 쓰면 모든 항목에.
코드
레시피 recipes/shadcn/components/ui/wheel-picker.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.
달만 고르기 — "완료" 로 넣는다
이 창의 폭으로 연다 — 1280 미만은 아래 시트, 이상은 칸 아래 팝오버. 휠이 멈춘 뒤 "완료" 로 넣는다.
import { WheelPicker, WheelPickerColumn } from "@/components/ui/wheel-picker"
<WheelPicker aria-label="월 선택">
<WheelPickerColumn aria-label="연도" options={years} align="right" className="w-[120px]" value={draft.year} onValueChange={(year) => setDraft({ ...draft, year })} />
<WheelPickerColumn aria-label="월" options={months} align="left" className="w-[96px]" loop value={draft.month} onValueChange={(month) => setDraft({ ...draft, month })} />
</WheelPicker>
<Button onClick={() => { setMonth(draft); setOpen(false) }}>완료</Button>Behavior
| 인터랙션 | 동작 |
|---|---|
| 손가락으로 밀기 | 브라우저 스크롤 · 스냅의 관성으로 굴러 가장 가까운 칸에 멈춘다. 멈춘 뒤 값이 정해진다 |
| 마우스로 끌기 | 3px 넘게 움직이면 끈다. 놓을 때 속도로 최대 3칸 더 가고 220 ~ 360ms 로 맞춘다. 끈 직후의 누름은 무시한다 |
| 마우스 휠 · 트랙패드 | 마지막 입력 120ms 뒤 가까운 칸으로 160ms |
| 보이는 항목 누르기 | 그 항목을 가운데로 200ms(ease-out cubic) — 멈춘 뒤 값이 정해진다. 웹도 브라우저의 부드러운 스크롤에 맡기지 않는다(2026-10-15 사용자 결정 — 구현 세션 질문 Q-005, 안 A) |
| 굴리는 동안 | 띠를 지나는 항목만 짙게 — 값은 멈춘 뒤 한 번만 바뀐다 |
| 반복 | 칼럼마다 — 끄면 처음 · 끝에서 멈추고, 켜면 끝 다음에 처음이 이어진다 |
| 막힘 | 값은 그대로, 굴릴 수도 초점이 갈 수도 없다 |
| 모션 줄이기 | 모든 이동을 바로 |
키보드
| 키 | 동작 |
|---|---|
↑ ↓ | 이전 · 다음 항목 — 누르기와 같은 200ms(누르고 있으면 바로바로 이어서) |
Home End | 처음 · 끝 항목 |
Tab | 다음 칼럼 |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1) | 고른 항목 fg-neutral 띠 위 15.20 · 다크 10.32 ✓. 둘레 항목은 고를 값이 아닌 표시(안개로 흐려진다) |
| WCAG 2.1.1 Keyboard | 칼럼마다 Tab 자리, ↑ ↓ Home End ✓ |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | medium 44 · small 36 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | medium 44 ✓ · small 36 ⚠ |
| 앱(Flutter 3.44.9) | Flutter 는 spinButton 역할을 구현하지 않았다(넣으면 디버그에서 오류가 난다 — 3.47 도 같다). 칼럼은 역할 없이 이름(칼럼 이름) + 값(항목 글) + 올리기 · 내리기 동작으로 둔다 — Android 는 이것을 조절 막대(SeekBar)로, iOS 는 조절 가능(adjustable)으로 읽어 위아래로 쓸어 값을 바꾼다. 모바일에서 spinbutton 에 해당하는 실제 표현이다. Flutter 가 spinButton 을 구현하면 역할 한 줄을 더한다 |
| ARIA | 휠 role="group" + 이름(필수), 칼럼 role="spinbutton" + 이름 · aria-valuetext(항목 글) · aria-valuemin 0 · aria-valuemax · aria-valuenow(번호). 항목은 보조 기술에 숨긴다 |
Do / Don't
✅ Do
- 시각 · 연 · 월처럼 순서가 있는 값.
- 휠이 멈춘 뒤에 값을 정하고, 시트 · 팝오버에서는 "완료" 로 넣는다.
- 칼럼마다 이름(연도 · 월 · 시 · 분).
❌ Don't
- 날짜를 연 · 월 · 일 세 휠로 받기 — 피한다. 먼 날도 달력 제목의 연 · 월 휠로.
- 순서 없는 긴 목록 — Select.
- 항목에 버튼 · 여러 줄 글.
Specification
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 높이height | 항목 높이 × 보이는 수medium 5개 220 · 7개 308, small 5개 180 |
| 배경background | bg-layer-floating#FFFFFF다크 #2D3346 | ||
column | enabled | 너비width | 항목 글 폭 + 좌우 16칼럼 묶음은 휠 가운데. 반복(loop)은 칼럼마다 — 끄면 첫 값 · 끝 값에서 멈춘다. 연 · 월 휠(달력 머리 · 달만 고르기)은 연 120 · 월 96 고정(date-picker.yaml) |
item | enabled | 좌우 여백paddingX | spacing-x416px |
| 글자·아이콘foreground | fg-disabled#8A91A0다크 #838997가운데가 아닌 항목 — 띠에 걸친 부분만 아래 고른 색으로 칠한다(굴리는 동안에도) | ||
| 굵기fontWeight | 500 | ||
| 고른 글자색selectedForeground | fg-neutral#1A1F2E다크 #F5F6FA | ||
| 숫자 폭numerals | tabular-nums숫자 폭을 같게 | ||
indicator | enabled | 띠 들임insetX | spacing-x416px휠 좌우 끝에서 16 들인다 |
| 모서리radius | radius-r28px | ||
| 배경background | bg-neutral-weak#F5F6FA다크 #353B4D | ||
fog | enabled | 높이height | min(휠 높이 × 40%, 항목 3칸)medium 5개 88 · 7개 123 · small 5개 72 — 위아래 끝에서 바탕색으로(gradient-fade-mask) |
focusRing | focused | 링 두께width | 2px |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | -2px가운데(고른) 항목 둘레 안쪽, 모서리 8 — 키보드로 들어올 때만. 터치 · 마우스로 굴리는 동안은 숨긴다 | ||
item | disabled | 고른 글자색selectedForeground | fg-disabled#8A91A0다크 #838997고른 항목도 흐리게 — 띠는 남는다 |
size=medium
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
item | enabled | 높이height | 44px |
| 글자typography | text-t10-static26 / 35 — 글자 크기 설정을 따르지 않는 px(웹 text-t10-static, 앱 TextScaler.noScaling — SEED t10-static), 휠 칸이 넘치지 않게 | ||
indicator | enabled | 높이height | 44px |
size=small
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
item | enabled | 높이height | 36px |
| 글자typography | text-t7-static20 / 27 — 글자 크기 설정을 따르지 않는 px(웹 text-t7-static, 앱 TextScaler.noScaling — SEED t7-static) | ||
indicator | enabled | 높이height | 36px |
SEED 와 다른 점
- 안개 높이는 늘 min(40%, 3칸) — SEED 는 연 · 월 휠에서 코드 상수 102 를 쓴다(식으로는 123).
- 기본 크기 medium — SEED rootage 는 small, 코드 · React 는 medium(코드를 따른다).
- 달만 고르기에 쓴다 — SEED 에는 월만 고르는 피커가 없다(연 · 월 휠은 Date Picker 안뿐).
- 링 색은 groo
stroke-focus-ring.
Migration notes
2026-10-30 — 달 고르기 시트 제목
캘린더 · 예산의 달 고르기 시트 제목을 "월 선택" 한 말로 맞췄다(2026-10-29 I-27 — 웹 · 앱 같게). 코드 예시의 휠 이름 · 사이트 그림의 시트 제목이 이미 "월 선택" 이라 "달만 고르기" 에 한 줄만 더했다. 값 변경 없음.
2026-10-22 — 세 휠은 "피한다"
"연 · 월 · 일 세 휠로 받지 않는다"(c64ff4e 세션 문장 — 10-03 비교 페이지에 "세 휠" 이 없다)를 "하루는 달력이 기본 · 먼 날은 달력 제목의 연 · 월 휠 · 세 휠은 피한다" 로 낮췄다(3B). 사용자 결정 — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L. 고르지 않은 안: Don't 그대로(3A) · 먼 날 칸은 세 휠 허용(3C). 지금 세 휠을 쓰는 곳은 없다 — 화면은 같다.
2026-10-22 — 앱 칼럼은 조절 가능으로(I-04 검수)
Flutter 3.44.9 는 spinButton 역할이 없다(넣으면 디버그에서 오류). 앱 칼럼은 이름 + 값 + 올리기 · 내리기로 두고(Android 조절 막대 · iOS 조절 가능), Flutter 가 역할을 구현하면 한 줄 더한다 — Accessibility 의 앱 줄.
2026-10-22 — "5개 이하" 를 판단 순서 링크로
"고를 것이 5개 이하면 Radio · Segmented Control" 은 결정 기록 없이 들어간 숫자라 Select 의 판단 순서(2 ~ 4 · 5 이상)와 어긋났다 — 그 절 링크로 바꿨다(사용자 결정 2B — https://claude.ai/artifact/SmHD31RQxTjWbfK83nbcym).
2026-10-15 — 누르기 · 키 이동은 200ms
구현 세션 질문 Q-005 — 항목을 눌러 가운데로 옮기는 시간이 "부드러운 스크롤" 로만 적혀 있어, 앱은 200ms ease-out cubic, 웹(레시피)은 브라우저 기본(크로미움 1칸 122 · 2칸 190 · 3칸 226ms, 브라우저마다 다르다)으로 갈렸다. 사용자가 200ms ease-out cubic(motion-duration-d4)으로 골랐다(안 A — 150ms(B) · 브라우저 기본(C) · 끌기 정착과 같은 220 + 칸당 40(D)은 고르지 않았다). 레시피는 scrollTo({ behavior: "smooth" }) 대신 끌기 정착에 쓰던 맞추기(스냅을 끄고 ease-out cubic)에 200ms 를 준다 — 키(↑ ↓ · Home · End)도 같다. 앱은 그대로다.
2026-10-08 — 글자 값을 고정 px 토큰으로
YAML 의 항목 글자가 rem 토큰($text-t10 · $text-t7)을 가리키고 "글자 크기 설정을 따르지 않는 px" 는 비고에만 있었다 — YAML 대로 만들면 글자 크기 200% 에서 휠 글자가 52 / 70 으로 커져 44 칸을 넘어 위아래 항목과 겹친다. 레시피 · 제품(웹 -static 클래스 · 앱 TextScaler.noScaling) · SEED rootage 와 같게 값 자리를 -static 토큰($text-t10-static · $text-t7-static)으로 고쳤다. 크기 · 모습은 그대로다. -static 은 같은 글자 토큰의 px 판이다(Typography v104 — 내보낼 때 생긴다). 사용자 결정 — 비교 페이지 3.
2026-10-03 — SEED Wheel Picker 로 새로 둔다
사용자가 비교 페이지에서 정했다 — 시각은 SEED Time Picker(이 휠 위), 달력의 연 · 월은 휠, 달만 고르기도 연 · 월 휠 + "완료".
제품은 앱 적용 단계에서 옮긴다(2026-10-02 조사).
- Desk 웹 — 월 피커 두 벌(
MonthPicker5곳 — 데스크톱 손 드롭다운 · 폰 Drawer, Esc 안 됨 · 포털 없음 · 이름 없는 연도 이동, 캘린더 머리 피커 — 다른 모양), 네이티브 month 1. 모두 4 × 3 격자를 누르면 바로 들어간다. - Desk 앱 — 월 시트 두 벌(72 × 36 · 75 × 34, 하나는 버튼이 아님), 시각은 iOS 휠(CupertinoDatePicker).
- HR 웹 — 연도 셀렉트 7 · 연 · 월 숫자칸 2.