DateRangePicker
시작일과 종료일로 이루어진 기간을 고릅니다. 달력이 두 개 나란히 놓이고, 두 번째 클릭 전에도 포인터를 따라 구간이 미리 표시됩니다.
import { DateRangePicker } from 'neba';
<DateRangePicker label="숙박" startPlaceholder="체크인" endPlaceholder="체크아웃" clearable />;Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'outline' | 'text' | 'outline' | 표면의 무게. 채움 / 하이라인 / 없음 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 높이와 타입 스케일 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음 |
| value | DateRange | null | — | 선택된 범위. { start, end }이고 각각 null일 수 있습니다 |
| defaultValue | DateRange | null | — | 초기 범위 |
| onValueChange | (value: DateRange) => void | — | 항상 객체로 불립니다. 비워진 범위는 { start: null, end: null } |
| defaultMonth | Date | — | 값이 없을 때 달력이 열리는 달. 기본값은 이번 달 |
| minDate | Date | null | — | 고를 수 있는 가장 이른 날. 두 패널 모두에 적용됩니다 |
| maxDate | Date | null | — | minDate의 반대쪽 끝 |
| shouldDisableDate | (date: Date) => boolean | — | 범위 안이지만 고를 수 없는 칸, 주말, 공휴일, 이미 예약된 방. 셀은 그리드에 남은 채 비활성이 되고, 인자로는 그 칸이 만들 값이 들어옵니다 |
| weekStartsOn공통 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | 주가 시작하는 요일. 일요일이 0이며, 기본값은 로케일을 따릅니다 |
| monthCount | 1 | 2 | 2 | 한 번에 보이는 달의 수. 달을 넘는 범위가 예외가 아니라 보통이라 2가 기본값입니다 |
| startPlaceholder | ReactNode | — | 시작이 비었을 때 트리거의 왼쪽 절반에 보이는 내용 |
| endPlaceholder | ReactNode | — | 끝이 비었을 때의 같은 것 |
| presets | readonly DateRangePreset[] | — | 달력 옆에 놓이는 단축 범위들, "최근 7일", "이번 달". value가 함수면 눌린 시점에 계산됩니다 |
| open | boolean | — | 팝업이 열려 있는지. onOpenChange와 함께 제어 컴포넌트로 씁니다 |
| defaultOpen | boolean | false | 처음에 열린 채로 시작 |
| onOpenChange | (open: boolean) => void | — | 열리거나 닫힐 때 |
| locale | string | — | BCP 47 태그. 월·요일 이름, 헤더의 연/월 버튼 순서, 트리거의 표기를 정합니다. 기본값은 브라우저의 로케일 |
| format | Intl.DateTimeFormatOptions | — | 트리거가 값을 쓰는 방식. Intl에 그대로 넘어갑니다 |
| clearable | boolean | false | 값을 비우는 ×를 트리거에 답니다 |
| closeOnSelect | boolean | true | 두 끝이 다 정해지면 팝업을 닫습니다 |
| labels | Partial<PickerLabels> | — | 스크린 리더가 읽는 문자열. 스무 개가 한 벌이라 prop 하나로 받습니다. 기본값은 locale의 표현이고, 날짜 이름은 Intl이 만듭니다 |
| name | string | — | 폼 제출 시의 필드 이름. 같은 이름의 hidden input 두 개가 나가므로 FormData.getAll로 받습니다 |
| fullWidth | boolean | false | 컨테이너 너비만큼 확장 |
| label | ReactNode | — | 컨트롤과 연결되는 라벨. Base UI Field가 묶어 줍니다 |
| description | ReactNode | — | 보조 설명 |
| error | ReactNode | — | 오류 메시지. 값이 있으면 invalid 상태도 함께 켜지고 색 계열이 danger로 넘어갑니다 |
| invalid | boolean | !!error | 메시지 없이 invalid만 켭니다. 외부 폼 라이브러리가 유효성을 가질 때 |
| readOnly | boolean | false | 값은 보이지만 바꿀 수 없음. 색과 가장자리는 유지한 채 채도만 빠집니다 |
| disabled | boolean | false | 사용 불가. 색 계열을 버리고 중립 회색이 됩니다 |
<div>의 native 속성은 root로 전달됩니다. color · defaultValue · children만 위 표와 이름이 겹쳐 제외됩니다.
값은 튜플이 아니라 객체 하나입니다.
interface DateRange {
start: Date | null;
end: Date | null;
}onValueChange는 항상 객체로 호출됩니다. 비워진 범위는 { start: null, end: null }이므로 두 종류의 "비어 있음"을 검사할 필요가 없습니다.
첫 클릭과 두 번째 클릭 사이의 절반 상태는 { start, end: null }로 보고되고, 두 번째 클릭 없이 팝업을 닫으면 버려집니다. 두 번째 클릭이 첫 번째보다 앞선 날짜여도 오류가 아니라 순서를 바꿔 말한 같은 범위로 보고 정렬합니다.
나머지 prop(minDate · maxDate · shouldDisableDate · variant · size)은 DatePicker와 동일하게 동작합니다.
예시
monthCount
기본값은 2입니다. 두 패널은 반으로 나뉜 하나의 달력이므로, 왼쪽에는 앞으로 가는 stepper가 없고 오른쪽에는 뒤로 가는 stepper가 없으며, 어느 쪽 헤더를 조작해도 두 패널이 함께 움직입니다.
패널이 둘일 때는 인접한 달의 앞뒤 날짜를 그리지 않습니다. 같은 날짜가 두 패널에 중복으로 나타나지 않게 하기 위한 것입니다.
presets
자주 쓰는 기간을 팝업 옆에 버튼으로 놓습니다. 프리셋의 value는 범위 객체이거나 범위를 반환하는 함수입니다. 함수 쪽을 쓰세요. 렌더 시점에 한 번 계산된 범위는 탭을 오래 열어 둔 사용자에게 틀린 값이 됩니다.
name
name을 주면 같은 이름의 hidden input 두 개가 그려지므로 두 끝이 함께 제출됩니다.
const form = new FormData(event.currentTarget);
const [start, end] = form.getAll('stay'); // '2026-07-03', '2026-07-09'두 값 모두 로컬 기준 YYYY-MM-DD이며, native <input type="date">가 제출하는 형식과 같습니다.
접근성
- 각 패널은
role="grid"이고 자기 tab 정지를 가지므로,Tab은 84개 칸을 지나지 않고 두 그리드 사이를 이동합니다. - 두 끝에만
aria-selected가 붙습니다. 사이의 날짜는 구간 표시만 받습니다. - 팝업 푸터가 다음 클릭이 어느 끝을 채울지 알려 줍니다.