DatePicker
input, trigger, popover, 캘린더 그리드로 이뤄진 단일 날짜 선택.

import { DatePicker } from '@kalyx/react';
기본 사용
import { useState } from 'react';
import { DatePicker, type ISODateString } from '@kalyx/react';
function Example() {
const [date, setDate] = useState<ISODateString | null>(null);
return (
<DatePicker value={date} onChange={setDate}>
<DatePicker.Input placeholder="YYYY-MM-DD" />
<DatePicker.Trigger />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
);
}
직접 사용해보기
라이브 에디터는
React와 모든 Kalyx 컴포넌트가 이미 스코프에 들어 있는 상태로 실행되므로import줄을 생략했습니다. 여러분의 프로젝트로 옮길 때는 import 를 함께 적어 주세요. 전체 import 는 위의 라이브가 아닌 블록에 있습니다.
function BasicDatePicker() { const [date, setDate] = React.useState(null); const [view, setView] = React.useState('days'); const headerCls = { header: 'kx-live-header', title: 'kx-live-title', navButton: 'kx-live-nav', }; return ( <DatePicker value={date} onChange={setDate}> <div className="kx-live-row"> <DatePicker.Input className="kx-live-input" placeholder="YYYY-MM-DD" /> <DatePicker.Trigger className="kx-live-trigger" aria-label="캘린더 열기" /> </div> <DatePicker.Popover className="kx-live-popover"> {view === 'days' && ( <DatePicker.Calendar onTitleClick={() => setView('months')} classNames={{ ...headerCls, grid: 'kx-live-grid', gridCell: 'kx-live-cell', weekdayHeader: 'kx-live-weekday', day: 'live-day', daySelected: 'live-day-selected', dayToday: 'live-day-today', dayDisabled: 'kx-live-disabled', dayOutsideMonth: 'kx-live-outside', }} /> )} {view === 'months' && ( <DatePicker.MonthGrid onSelect={() => setView('days')} onTitleClick={() => setView('years')} classNames={{ ...headerCls, grid: 'kx-live-month-grid', month: 'kx-live-my-cell', monthSelected: 'kx-live-my-selected', monthCurrent: 'kx-live-my-current', }} /> )} {view === 'years' && ( <DatePicker.YearGrid onSelect={() => setView('months')} classNames={{ ...headerCls, grid: 'kx-live-year-grid', year: 'kx-live-my-cell', yearSelected: 'kx-live-my-selected', yearCurrent: 'kx-live-my-current', }} /> )} </DatePicker.Popover> <div className="kx-live-value"> 선택된 값: <code>{date ?? 'null'}</code>. 월 타이틀을 클릭하면 월 / 연도 뷰로 넘어갑니다. </div> </DatePicker> ); }
<DatePicker> (Root)
상태를 보관하고 서브 컴포넌트에 컨텍스트를 제공합니다. value 제공 시 제어형, defaultValue만 제공 시 비제어형.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | ISODateString | null | - | 제어형 선택 날짜. |
defaultValue | ISODateString | - | 비제어형 초기값. value 있으면 무시. |
onChange | (value: ISODateString | null) => void | - | 선택 또는 초기화 시 호출. |
disabled | DisabledRule[] | boolean | false | 특정 날짜 비활성화 또는 전체 비활성. |
readOnly | boolean | false | 변경 방지, 표시용으로 선택 가능. |
weekStartsOn | 0 | 1 | locale 에서 추론 | 0 = 일요일, 1 = 월요일. 생략하면 locale 에서 추론한다(en-US → 0, de-DE → 1). 명시한 prop 이 항상 우선한다. |
displayFormat | string | 'yyyy-MM-dd' | date-fns 포맷 문자열. |
locale | string | 'en-US' | BCP 47 로케일 태그. |
dir | 'ltr' | 'rtl' | 'ltr' | 레이아웃 방향. 'rtl'이면 캘린더 그리드에 dir="rtl"이 붙고 ArrowLeft/ArrowRight가 시각적 레이아웃을 따르도록 반전된다(WAI-ARIA grid 패턴). ArrowUp/Down, PageUp/Down, Home/End는 논리적 방향을 유지한다. 국제화 참고. |
displayTimezone | string | - | IANA 타임존(예: "Asia/Seoul"). 설정하면 Input 이 이 타임존으로 포맷하고, Calendar 하이라이트가 그 타임존의 civil day 와 맞으며, onChange 는 이 타임존의 civil midnight 을 내보낸다. Timezone 참고. |
labels | Partial<DatePickerLabels> | - | ARIA 라벨 재정의(기본값은 영어). 키: triggerOpen, triggerClose, popoverLabel, prevMonth, nextMonth, prevYear, nextYear 등. |
adapter | DateAdapter | DateFnsAdapter | 커스텀 날짜 어댑터. |
children | ReactNode | - | 서브 컴포넌트. |
DisabledRule
type DisabledRule =
| { date: ISODateString } // 특정 일자
| { before: ISODateString } // 그 이전 모든 날짜
| { after: ISODateString } // 그 이후 모든 날짜
| { dayOfWeek: number[] }; // 0 = 일 … 6 = 토
주말과 오늘 이전 모든 날짜 비활성화:
<DatePicker
value={iso}
onChange={setIso}
disabled={[{ dayOfWeek: [0, 6] }, { before: new Date().toISOString() }]}>
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
function WeekdayOnly() { const [date, setDate] = React.useState(null); const today = new Date().toISOString(); return ( <DatePicker value={date} onChange={setDate} disabled={[{ dayOfWeek: [0, 6] }, { before: today }]} > <DatePicker.Input className="kx-live-input" placeholder="오늘부터, 평일만" /> <DatePicker.Popover className="kx-live-popover"> <DatePicker.Calendar classNames={{ header: 'kx-live-header', title: 'kx-live-title', navButton: 'kx-live-nav', grid: 'kx-live-grid', gridCell: 'kx-live-cell', weekdayHeader: 'kx-live-weekday', day: 'live-day', daySelected: 'live-day-selected', dayToday: 'live-day-today', dayDisabled: 'kx-live-disabled', dayOutsideMonth: 'kx-live-outside', }} /> </DatePicker.Popover> </DatePicker> ); }
<DatePicker.Input>
<input role="combobox">를 렌더합니다. blur/Enter에서 타이핑된 날짜를 파싱합니다. value, onChange, type을 제외한 모든 표준 input 속성을 확장합니다.
| Prop | 타입 | 설명 |
|---|---|---|
format | string | Root의 displayFormat 오버라이드. |
내부 <input>으로 ref가 전달됩니다.
<DatePicker.Trigger>
popover를 토글하는 버튼. children이 없으면 기본 캘린더 아이콘을 렌더합니다. type을 제외한 모든 표준 버튼 속성을 확장합니다.
| Prop | 타입 | 설명 |
|---|---|---|
children | ReactNode | 기본 아이콘 오버라이드. |
<DatePicker.Trigger aria-label="캘린더 열기">
📅
</DatePicker.Trigger>
<DatePicker.Popover>
Floating UI 위치 계산 포털 (role="dialog", aria-modal="false"). 바깥 클릭 해제, Escape 닫기, 포커스 복원을 처리합니다.
role을 제외한 표준 <div> 속성 확장.
<DatePicker.Popover className="rounded-lg border bg-white p-3 shadow-lg">
<DatePicker.Calendar />
</DatePicker.Popover>
<DatePicker.Calendar>
월 그리드. 완전한 키보드 네비게이션 (접근성 참조).
| Prop | 타입 | 설명 |
|---|---|---|
classNames | DatePickerCalendarClassNames | 내부 슬롯 스타일. |
fixedWeeks | boolean (기본 false) | 항상 6주 행을 렌더. 지정하지 않으면 4~6행이라 달마다 popover 높이가 바뀐다. |
showWeekNumber | boolean (기본 false) | 그리드 왼쪽에 ISO 8601 주차 열(1~53)을 렌더한다. 이 열은 WAI-ARIA grid 데이터 영역 밖의 <th scope="row"> 라 날짜 셀 키보드 내비게이션에 영향을 주지 않는다. classNames 의 weekNumberHeader / weekNumber 키로 스타일링한다. |
onTitleClick | () => void | 월/연 타이틀 클릭 시. MonthGrid/YearGrid와 연결. |
classNames 키
type DatePickerCalendarClassNames = {
root?: string;
header?: string; // 타이틀 + nav 버튼 감싸기
title?: string; // "2026년 4월"
navButton?: string; // 이전 / 다음 버튼
grid?: string; // <table role="grid">
gridRow?: string; // <tr>
gridCell?: string; // <td role="gridcell">
day?: string; // 날짜 버튼
daySelected?: string; // day.isSelected일 때
dayToday?: string; // day.isToday일 때
dayDisabled?: string; // day.isDisabled일 때
dayOutsideMonth?: string;// 6주 뷰 패딩
weekdayHeader?: string; // "월", "화", …
};
data-* 속성
각 날짜 버튼은 스타일링에 쓸 수 있는 상태 속성을 내보냅니다(Tailwind 의 data-[selected]:, 일반 CSS 의 [data-selected]). 이 속성은 상태가 활성일 때만 붙습니다. 전체 계약은 스타일링 을 참고하세요.
| 속성 | 활성 조건 |
|---|---|
data-selected | 날짜가 선택된 날짜임. |
data-today | 날짜가 오늘임. |
data-focused | 날짜가 키보드 포커스를 가짐. |
data-outside-month | 날짜가 인접한 달에 속함. |
비활성 날짜는 네이티브 disabled 속성과 aria-disabled 를 사용합니다(:disabled 또는 dayDisabled 슬롯으로 스타일링하세요).
<DatePicker.MonthGrid> (선택)
3×4 월 그리드. 월 바로 이동이 필요할 때 mount.
| Prop | 타입 | 설명 |
|---|---|---|
classNames | DatePickerMonthGridClassNames | 스타일. |
onSelect | () => void | 월 선택 후 호출. 보통 Calendar로 복귀. |
onTitleClick | () => void | 연도 타이틀 클릭. YearGrid와 연결. |
type DatePickerMonthGridClassNames = {
root?: string;
header?: string;
title?: string;
navButton?: string;
grid?: string;
month?: string;
monthSelected?: string;
monthCurrent?: string;
};
각 월 버튼은 data-selected(선택된 월), data-current(현재 월), data-focused(키보드 포커스)를 내보냅니다. 스타일링 참고.
<DatePicker.YearGrid> (선택)
페이지네이션되는 연도 그리드 (12년 단위).
| Prop | 타입 | 설명 |
|---|---|---|
classNames | DatePickerYearGridClassNames | 스타일. |
onSelect | () => void | 연도 선택 후 호출. |
type DatePickerYearGridClassNames = {
root?: string;
header?: string;
title?: string;
navButton?: string;
grid?: string;
year?: string;
yearSelected?: string;
yearCurrent?: string;
};
각 연도 버튼은 data-selected, data-current, data-focused 를 내보냅니다. MonthGrid 와 같은 계약입니다.
패턴
월 / 연도 네비게이션
import { useState } from 'react';
import { DatePicker } from '@kalyx/react';
function WithJump() {
const [view, setView] = useState<'days' | 'months' | 'years'>('days');
return (
<DatePicker value={iso} onChange={setIso}>
<DatePicker.Input />
<DatePicker.Popover>
{view === 'days' && (
<DatePicker.Calendar onTitleClick={() => setView('months')} />
)}
{view === 'months' && (
<DatePicker.MonthGrid
onSelect={() => setView('days')}
onTitleClick={() => setView('years')}
/>
)}
{view === 'years' && (
<DatePicker.YearGrid onSelect={() => setView('months')} />
)}
</DatePicker.Popover>
</DatePicker>
);
}
이 페이지 위쪽의 "직접 사용해보기" 예제가 이미 이 흐름을 연결해 둔 것입니다. 월 타이틀을 누르면 MonthGrid, 연도를 누르면 YearGrid 가 열립니다.
비제어 + 폼 제출
<form action="/api/save" method="post">
<DatePicker defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input name="startDate" />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
<button type="submit">저장</button>
</form>
최소 / 최대 날짜
별도의 minDate/maxDate prop은 없습니다. 동일한 규칙을 disabled로 표현하세요.
<DatePicker
disabled={[
{ before: '2026-01-01T00:00:00.000Z' },
{ after: '2026-12-31T00:00:00.000Z' },
]}
value={iso}
onChange={setIso}>
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
:::caution disabled 경계값은 displayTimezone과 짝을 맞추세요
disabled 규칙은 캘린더 좌표가 아니라 instant를 비교합니다. displayTimezone을 쓰지 않으면 둘이 같은 것이라 위 예제가 정확합니다.
displayTimezone을 켜는 순간, 손으로 쓴 '2026-01-01T00:00:00.000Z'는 더 이상 그 존의 civil 1월 1일이 아닙니다. 음수 UTC offset에서는 현지 기준 아직 12월 31일이고, 큰 양수 offset에서는 현지 자정보다 한참 전에 이미 1월 1일입니다. 그래서 경계일이 규칙의 반대편에 놓일 수 있습니다.
피커가 실제로 내보내는 것과 같은 종류의 값을 넘기세요. onChange로 받은 instant, 또는 civilMidnightFromUtcDay로 만든 값입니다.
import { civilMidnightFromUtcDay } from '@kalyx/core';
const tz = 'Asia/Seoul';
<DatePicker
displayTimezone={tz}
disabled={[{ before: civilMidnightFromUtcDay('2026-01-01T00:00:00.000Z', tz) }]}
value={iso}
onChange={setIso}>
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
isDateDisabled를 직접 호출할 때도 같은 규칙이 적용됩니다. 커스텀 그리드의 셀별 상태는 getCalendarDays가 미리 계산해 둔 isDisabled 플래그를 쓰세요. 셀마다 이미 정규화돼 있습니다.
:::