Groo Design

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이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다.

9:41
예산
2026년 10월
월 선택
9:41
일정 추가
오후 3:00
시간 선택
9:41
예산
2026년 10월
월 선택
9:41
일정 추가
오후 3:00
시간 선택
월 고르기 · 시각 · 달력의 연 · 월 휠 — 라이트 · 다크

직접 골라 보기

크기 · 보이는 칸 수 · 칼럼을 고르면 스펙대로 그린 휠이 바뀐다. 실제로 굴리고 누를 수 있다.

고른 값: 2026년 10월 — 값은 휠이 멈춘 뒤 한 번만 바뀌어요.

크기 size
보이는 칸 visibleItems
월 반복 loop
모드
브랜드
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 — 위아래 안개
휠은 칼럼 · 항목 · 가운데 띠 · 안개
부위설명
ⓐ Column칼럼 — 항목 글 폭만큼. 칼럼 묶음은 휠 가운데.
ⓑ Item항목 — 짧은 한 줄 글.
ⓒ Indicator선택 띠 — 가운데 줄. 띠에 걸친 글자만 짙게 칠한다.
ⓓ Fog안개 — 위아래 끝이 바탕으로 흐려진다.

부위

부위설명
root휠 — 칼럼 묶음을 가운데에 모은다
column칼럼 — 항목 글 폭만큼(좌우 여백 포함). 칼럼마다 Tab 자리 하나
item항목 — 짧은 한 줄 글(숫자 · 짧은 낱말). 버튼 · 링크 · 여러 줄은 넣지 않는다
indicator선택 띠 — 가운데 줄, 칼럼을 모두 가로지른다
fog안개 — 위아래 끝이 바탕으로 흐려진다
focusRing키보드 포커스 링(웹)

Properties

Size

medium(44 · 글자 26)이 기본이다 — 시각 · 월 고르기 · 연 · 월 휠. small(36 · 글자 20)은 좁은 팝오버에만 쓴다. 글자는 글자 크기 설정을 따르지 않는 px 이다 — 휠 칸이 넘치지 않게.

small 36글자 20 / 27 · 5칸 180 · 안개 72
medium 44(기본)글자 26 / 35 · 5칸 220 · 안개 88

small 은 좁은 팝오버에만 쓴다 — 누르는 높이 36 은 AAA(44)에 못 미친다. 달력 머리의 연 · 월 휠은 medium · 7칸(308 · 안개 123)이다.

small · medium

크기

sizeitem 높이item 글자indicator 높이설명
small36pxtext-t7-static 20px / 27px — 20 / 27 — 글자 크기 설정을 따르지 않는 px(웹 text-t7-static, 앱 TextScaler.noScaling — SEED t7-static)36px36 — 좁은 자리(작은 팝오버)
medium (기본)44pxtext-t10-static 26px / 35px — 26 / 35 — 글자 크기 설정을 따르지 않는 px(웹 text-t10-static, 앱 TextScaler.noScaling — SEED t10-static), 휠 칸이 넘치지 않게44px44 — 기본(시각 · 월 고르기 · 연 · 월 휠)

보이는 칸 수

기본 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 · 다크 #F5F6FA2px-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 — 웹 · 앱 같게).

9:41
예산
2026년 10월
월 선택
이렇게 — 연 | 월 휠 + "완료" — 달력 머리의 연 · 월 휠과 같은 모양, 연을 넘나들기 쉽다
9:41
예산
2026년 10월
월 선택
‹2026년›
1월2월3월4월5월6월7월8월9월10월11월12월
이렇게 하지 않는다 — 4 × 3 월 격자 — 누르면 바로 들어가고, 연은 화살표로 한 해씩만 넘긴다
예산 월 — 시트의 연 · 월 휠

항목

짧은 한 줄 글 — 숫자 · 짧은 낱말. 시 · 분처럼 숫자만으로 읽히는 칼럼은 단위를 칼럼 이름으로 알리고 항목마다 붙이지 않는다. 연 · 월은 날짜 표기 그대로 "2026년" · "10월" 이다(달력의 연 · 월 휠과 같다). 버튼 · 링크 · 여러 줄 글은 넣지 않는다. 아이콘을 쓰면 모든 항목에.

코드

레시피 recipes/shadcn/components/ui/wheel-picker.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.

달만 고르기 — "완료" 로 넣는다

2026년 10월

이 창의 폭으로 연다 — 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

부위상태속성값
rootenabled높이height항목 높이 × 보이는 수medium 5개 220 · 7개 308, small 5개 180
배경backgroundbg-layer-floating#FFFFFF다크 #2D3346
columnenabled너비width항목 글 폭 + 좌우 16칼럼 묶음은 휠 가운데. 반복(loop)은 칼럼마다 — 끄면 첫 값 · 끝 값에서 멈춘다. 연 · 월 휠(달력 머리 · 달만 고르기)은 연 120 · 월 96 고정(date-picker.yaml)
itemenabled좌우 여백paddingXspacing-x416px
글자·아이콘foregroundfg-disabled#8A91A0다크 #838997가운데가 아닌 항목 — 띠에 걸친 부분만 아래 고른 색으로 칠한다(굴리는 동안에도)
굵기fontWeight500
고른 글자색selectedForegroundfg-neutral#1A1F2E다크 #F5F6FA
숫자 폭numeralstabular-nums숫자 폭을 같게
indicatorenabled띠 들임insetXspacing-x416px휠 좌우 끝에서 16 들인다
모서리radiusradius-r28px
배경backgroundbg-neutral-weak#F5F6FA다크 #353B4D
fogenabled높이heightmin(휠 높이 × 40%, 항목 3칸)medium 5개 88 · 7개 123 · small 5개 72 — 위아래 끝에서 바탕색으로(gradient-fade-mask)
focusRingfocused링 두께width2px
링 색colorstroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84
링 간격offset-2px가운데(고른) 항목 둘레 안쪽, 모서리 8 — 키보드로 들어올 때만. 터치 · 마우스로 굴리는 동안은 숨긴다
itemdisabled고른 글자색selectedForegroundfg-disabled#8A91A0다크 #838997고른 항목도 흐리게 — 띠는 남는다

size=medium

부위상태속성값
itemenabled높이height44px
글자typographytext-t10-static26 / 35 — 글자 크기 설정을 따르지 않는 px(웹 text-t10-static, 앱 TextScaler.noScaling — SEED t10-static), 휠 칸이 넘치지 않게
indicatorenabled높이height44px

size=small

부위상태속성값
itemenabled높이height36px
글자typographytext-t7-static20 / 27 — 글자 크기 설정을 따르지 않는 px(웹 text-t7-static, 앱 TextScaler.noScaling — SEED t7-static)
indicatorenabled높이height36px

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 웹 — 월 피커 두 벌(MonthPicker 5곳 — 데스크톱 손 드롭다운 · 폰 Drawer, Esc 안 됨 · 포털 없음 · 이름 없는 연도 이동, 캘린더 머리 피커 — 다른 모양), 네이티브 month 1. 모두 4 × 3 격자를 누르면 바로 들어간다.
  • Desk 앱 — 월 시트 두 벌(72 × 36 · 75 × 34, 하나는 버튼이 아님), 시각은 iOS 휠(CupertinoDatePicker).
  • HR 웹 — 연도 셀렉트 7 · 연 · 월 숫자칸 2.

이 페이지