본문으로 건너뛰기

DatePicker

달력 팝업에서 날짜 하나를 고릅니다. 월 이름과 연도가 각각 자기 그리드를 여는 버튼이므로 먼 과거나 미래에도 몇 번의 클릭으로 닿습니다. granularity를 주면 날짜 대신 한 달이나 한 해를 묻습니다.

tsx
import { DatePicker } from 'neba';

<DatePicker label="출고일" placeholder="날짜를 고르세요" clearable />;

Props

Prop타입기본값설명
variant공통'solid' | 'outline' | 'text''outline'표면의 무게. TextField·Select와 같은 셸입니다
size공통'xs' | 'sm' | 'md' | 'lg' | 'xl''md'높이와 타입 스케일. 달력의 한 칸도 같은 사다리를 씁니다. md는 32px
color공통'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'의미론적 색 역할. 임의 색상값은 받지 않습니다
density공통'default' | 'compact''default'여백만 바꿉니다. 높이와 글자 크기는 그대로
elevation공통0 | 1 | 2 | 30트리거의 그림자 깊이. 팝업은 자기 그림자를 따로 가집니다
valueDate | null선택된 날. onValueChange와 함께 제어 컴포넌트로 씁니다
defaultValueDate | null초기 값
onValueChange(value: Date | null) => void선택이 바뀔 때
granularity'day' | 'month' | 'year''day'무엇을 묻는지, 하루, 한 달, 한 해. month와 year는 그 그리드에서 열려 거기서 끝나고, 값은 고른 단위의 첫날(3월 1일, 1월 1일)이 됩니다. format 기본값과 푸터 버튼, name이 제출하는 문자열, 아래 세 프롭이 읽는 단위가 모두 여기를 따릅니다
defaultMonthDate값이 없을 때 달력이 열리는 달. 기본값은 이번 달
minDateDate | null고를 수 있는 가장 이른 날. granularity 단위로 비교하므로 3월 15일이 최솟값이면 day에서는 14일이 빠지고 month에서는 3월이 남습니다
maxDateDate | nullminDate의 반대쪽 끝
shouldDisableDate(date: Date) => boolean범위 안이지만 고를 수 없는 칸, 주말, 공휴일, 이미 예약된 방. 셀은 그리드에 남은 채 비활성이 되고, 인자로는 그 칸이 만들 값이 들어옵니다
weekStartsOn공통0 | 1 | 2 | 3 | 4 | 5 | 6주가 시작하는 요일. 일요일이 0이며, 기본값은 로케일을 따릅니다
showTodayButtonbooleantrue푸터에 지금 단위로 가는 단축 버튼, granularity에 따라 오늘·이번 달·올해
openboolean팝업이 열려 있는지. onOpenChange와 함께 제어 컴포넌트로 씁니다
defaultOpenbooleanfalse처음에 열린 채로 시작
onOpenChange(open: boolean) => void열리거나 닫힐 때
localestringBCP 47 태그. 월·요일 이름, 헤더의 연/월 버튼 순서, 트리거의 표기를 정합니다. 기본값은 브라우저의 로케일
formatIntl.DateTimeFormatOptions트리거가 값을 쓰는 방식. Intl에 그대로 넘어갑니다
placeholderReactNode아무것도 고르지 않았을 때 트리거에 보이는 내용
clearablebooleanfalse값을 비우는 ×를 트리거에 답니다
closeOnSelectbooleantrue날을 고르는 즉시 팝업을 닫습니다. 물어본 것이 하나뿐이므로 기본값이 true입니다
labelsPartial<PickerLabels>스크린 리더가 읽는 문자열. 스무 개가 한 벌이라 prop 하나로 받습니다. 기본값은 locale의 표현이고, 날짜 이름은 Intl이 만듭니다
namestring폼 제출 시의 필드 이름. 값은 granularity를 따라 YYYY-MM-DD · YYYY-MM · YYYY로, UTC가 아니라 로컬 기준입니다
fullWidthbooleanfalse컨테이너 너비만큼 확장
labelReactNode컨트롤과 연결되는 라벨. Base UI Field가 묶어 줍니다
descriptionReactNode보조 설명
errorReactNode오류 메시지. 값이 있으면 invalid 상태도 함께 켜지고 색 계열이 danger로 넘어갑니다
invalidboolean!!error메시지 없이 invalid만 켭니다. 외부 폼 라이브러리가 유효성을 가질 때
readOnlybooleanfalse값은 보이지만 바꿀 수 없음. 색과 가장자리는 유지한 채 채도만 빠집니다
disabledbooleanfalse사용 불가. 색 계열을 버리고 중립 회색이 됩니다

<div>의 native 속성은 root로 전달됩니다. color · defaultValue · children만 위 표와 이름이 겹쳐 제외됩니다.

value의 타입은 Date | null입니다. 별도의 날짜 라이브러리는 쓰지 않습니다.

모든 비교는 UTC 타임스탬프가 아니라 로컬 달력의 날을 기준으로 합니다. 폼이 제출하는 hidden input도 로컬 기준 문자열(날짜라면 YYYY-MM-DD)이므로, toISOString()으로 하루가 밀리는 문제가 생기지 않습니다.

세 가지 뷰

헤더의 두 버튼이 각각 다른 그리드를 엽니다.

  • 월 이름: 12개월 그리드
  • 연도: 12년 그리드. 이때 stepper는 한 페이지씩 움직입니다.

연도를 고르면 월 뷰로 넘어갑니다. 두 버튼의 순서는 locale을 따르므로 한국어에서는 2026년 7월로 표시됩니다. 세 뷰의 너비와 높이가 같아서 뷰를 바꿔도 팝업 크기가 변하지 않습니다.

예시

variant

TextField와 같은 세 가지 무게를 같은 shell 위에 그립니다.

size

달력의 한 칸이 컨트롤 높이 단계를 씁니다. md에서 32px로, 같은 sizeButton이나 TextField와 같습니다.

granularity

granularity는 세 그리드 중 어디에서 멈출 수 있는지를 정합니다. monthyear에서는 달력이 그 그리드에서 열리고 거기서의 클릭이 곧 답입니다. 내려갈 day 뷰 자체가 없습니다.

값은 그대로 Date이고, 고른 단위의 첫날로 정규화됩니다. 3월이면 3월 1일, 2026년이면 1월 1일입니다. 나머지 셋이 여기를 따라갑니다. 트리거의 기본 format{ year: 'numeric', month: 'long' }이나 { year: 'numeric' }이 되고, 푸터의 단축 버튼이 "This month"나 "This year"가 되며, nameYYYY-MM이나 YYYY로 제출합니다. native <input type="month">가 제출하는 모양이지, 아무도 고르지 않은 날이 아닙니다.

위로 올라가는 길은 그대로이므로, month picker에서도 어느 해 어느 달이든 두 번의 클릭으로 닿습니다.

minDate · maxDate · shouldDisableDate

minDatemaxDategranularity 단위로, 한 칸이 대표하는 기간 전체와 비교합니다. day에서는 27일 09:00이 최댓값이어도 27일을 고를 수 있고, month에서는 3월 15일이 최솟값이어도 3월이 남습니다. 3월의 일부가 허용되기 때문입니다. 범위 안에 있지만 고를 수 없는 칸은 shouldDisableDate로 막습니다. 인자로는 그 칸이 만들 값이 들어오므로 month에서는 1일을 받습니다.

막힌 칸은 그리드에 남아 있고 disabled 속성 대신 aria-disabled로 표시됩니다. 방향키 이동 경로에서 빠지지 않게 하기 위한 것입니다.

disabled · readOnly · error

showTodayButton과 clearable

showTodayButton은 팝업 푸터에 지금 단위로 이동하는 버튼을 붙입니다. granularity에 따라 오늘, 이번 달, 올해입니다. clearable은 trigger에 값을 비우는 버튼을 붙입니다.

키보드

trigger는 텍스트 입력이 아니라 버튼입니다. 날짜는 달력에서 고릅니다.

동작
Space / Enter달력을 열고, 선택된 칸에 focus
하루 또는 한 주 이동, 가장자리에서 달 넘김
Home / End주의 처음 또는 끝
PageUp / PageDown한 달씩: Shift와 함께면 1년씩
Escape고르지 않고 닫기

그리드 전체가 tab 정지 하나이므로 Tab은 42개 칸을 지나지 않고 그리드를 빠져나갑니다.

접근성

  • 그리드는 role="grid", 각 칸은 role="gridcell" 버튼입니다. 칸의 이름은 숫자가 아니라 완전한 날짜입니다.
  • 선택된 칸은 aria-selected, 지금에 해당하는 날·달·해는 aria-current="date"와 숫자 아래의 점으로 표시됩니다.
  • label이 trigger의 accessible name이 되고, descriptionerroraria-describedby로 연결됩니다.
  • 팝업은 <body> 끝으로 portal되며 positioner에 neba-portal 클래스가 붙습니다.

Released under the MIT License