본문으로 건너뛰기

DatePicker

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

DatePicker 데모: popover를 열고 날짜를 선택
화면의 스타일은 데모용입니다. Kalyx는 CSS를 전혀 포함하지 않습니다.
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>
  );
}
결과
Loading...

<DatePicker> (Root)​

상태를 보관하고 서브 컴포넌트에 컨텍스트를 제공합니다. value 제공 시 제어형, defaultValue만 제공 시 비제어형.

Prop타입기본값설명
valueISODateString | null-제어형 선택 날짜.
defaultValueISODateString-비제어형 초기값. value 있으면 무시.
onChange(value: ISODateString | null) => void-선택 또는 초기화 시 호출.
disabledDisabledRule[] | booleanfalse특정 날짜 비활성화 또는 전체 비활성.
readOnlybooleanfalse변경 방지, 표시용으로 선택 가능.
weekStartsOn0 | 1locale 에서 추론0 = 일요일, 1 = 월요일. 생략하면 locale 에서 추론한다(en-US → 0, de-DE → 1). 명시한 prop 이 항상 우선한다.
displayFormatstring'yyyy-MM-dd'date-fns 포맷 문자열.
localestring'en-US'BCP 47 로케일 태그.
dir'ltr' | 'rtl''ltr'레이아웃 방향. 'rtl'이면 캘린더 그리드에 dir="rtl"이 붙고 ArrowLeft/ArrowRight가 시각적 레이아웃을 따르도록 반전된다(WAI-ARIA grid 패턴). ArrowUp/Down, PageUp/Down, Home/End는 논리적 방향을 유지한다. 국제화 참고.
displayTimezonestring-IANA 타임존(예: "Asia/Seoul"). 설정하면 Input 이 이 타임존으로 포맷하고, Calendar 하이라이트가 그 타임존의 civil day 와 맞으며, onChange 는 이 타임존의 civil midnight 을 내보낸다. Timezone 참고.
labelsPartial<DatePickerLabels>-ARIA 라벨 재정의(기본값은 영어). 키: triggerOpen, triggerClose, popoverLabel, prevMonth, nextMonth, prevYear, nextYear 등.
adapterDateAdapterDateFnsAdapter커스텀 날짜 어댑터.
childrenReactNode-서브 컴포넌트.

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>
  );
}
결과
Loading...

<DatePicker.Input>​

<input role="combobox">를 렌더합니다. blur/Enter에서 타이핑된 날짜를 파싱합니다. value, onChange, type을 제외한 모든 표준 input 속성을 확장합니다.

Prop타입설명
formatstringRoot의 displayFormat 오버라이드.

내부 <input>으로 ref가 전달됩니다.

<DatePicker.Trigger>​

popover를 토글하는 버튼. children이 없으면 기본 캘린더 아이콘을 렌더합니다. type을 제외한 모든 표준 버튼 속성을 확장합니다.

Prop타입설명
childrenReactNode기본 아이콘 오버라이드.
<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타입설명
classNamesDatePickerCalendarClassNames내부 슬롯 스타일.
fixedWeeksboolean (기본 false)항상 6주 행을 렌더. 지정하지 않으면 4~6행이라 달마다 popover 높이가 바뀐다.
showWeekNumberboolean (기본 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타입설명
classNamesDatePickerMonthGridClassNames스타일.
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타입설명
classNamesDatePickerYearGridClassNames스타일.
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 플래그를 쓰세요. 셀마다 이미 정규화돼 있습니다.

:::

관련​