Field.DatePicker

Field.DatePicker는 Field 안에서 Calendar를 Popover로 여는 버튼입니다. value는 Temporal.PlainDate나 null이고, onValueChange는 달력에서 고른 날짜를 받으며, 그러면 달력이 닫힙니다. 달력의 min, max, isDateDisabled, firstDayOfWeek, locale을 받습니다.

이 페이지의 내용
const id = useId();
const [date, setDate] = useState<Temporal.PlainDate | null>(null);
const [text, setText] = useState('');
const [invalid, setInvalid] = useState(false);

<Field.Group>
  <Label.Root htmlFor={id}>Start date</Label.Root>
  <Field.Root id={id} invalid={invalid}>
    <Field.TextInput
      value={text}
      onChange={(event) => {
        setText(event.target.value);
        setDate(parseDate(event.target.value));
        setInvalid(false);
      }}
      onBlur={() => {
        if (date) setText(formatDate(date));
        else setInvalid(text.trim() !== '');
      }}
    />
    <Field.DatePicker
      value={date}
      onValueChange={(next) => {
        setDate(next);
        setText(formatDate(next));
        setInvalid(false);
      }}
      showValue={false}
    />
  </Field.Root>
</Field.Group>

여기서는 같은 필드의 Field.TextInput으로 날짜를 입력할 수도 있습니다. 고른 날짜는 formatDate로 텍스트에 쓰이고, parseDate가 날짜로 읽은 입력은 달력을 그 달로 옮깁니다. Field.TextInput 옆에서는 그 입력이 필드의 컨트롤입니다. 입력이 필드의 id, name, 설명을 받고, 필드의 다른 곳을 누르면 입력에 포커스가 갑니다. showValue={false}이면 버튼은 달력 글리프만 보여 줍니다. 버튼은 자신의 name이 있으면 그 이름으로 폼에 날짜를 보냅니다. 없으면 필드의 name으로 보내는데, Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.TimePicker, Field.DateTimePicker가 아닌 Field 파트나 useField로 연결한 컨트롤이 그 name을 가지고 있으면 보내지 않습니다. 자신의 name이 없는 Field.TextInput이 그런 경우입니다. 입력한 텍스트 대신 2026-10-13 형식으로 날짜를 보내려면 Root 대신 선택기에 name을 두세요.

입력한 날짜

formatDate(date, { locale })는 날짜를 로캘의 숫자로 씁니다. 예: en-US에서는 10/13/2026, de에서는 13.10.2026, ko에서는 2026. 10. 13.. parseDate(text, { locale })는 입력한 텍스트를 Temporal.PlainDate로 읽고, 날짜가 아니면 null을 돌려줍니다. 숫자로 쓴 날짜는 formatDate가 쓰는 순서나 로캘의 short 날짜 스타일의 순서로 읽습니다. 예: formatDate가 2026-10-13으로 쓰는 gsw에서 13.10.26. 두 순서 모두 날짜로 읽히면 네 자리 이상의 연도는 formatDate의 순서로 읽고, 한두 자리 연도는 short 스타일이 연도를 두 자리로 쓸 때 그 스타일의 순서로 읽습니다. 글자가 있는 날짜는 Intl.DateTimeFormat이 로캘의 medium, long, full 날짜 스타일로 쓰는 대로 읽습니다. 예: en-US에서는 Oct 13, 2026, Tuesday, October 13, 2026. 대소문자는 상관없고, 숫자는 어느 문자 체계의 숫자여도 되며, 부분 사이는 어떤 문장 부호, 기호, 공백으로 나눠도 됩니다. 달은 Oct나 October처럼 긴 이름이든 짧은 이름이든 됩니다. 요일은 길게 쓰든 짧게 쓰든 빼든 되지만, 그 날짜의 요일이어야 합니다. 13 de octubre de 2026의 de처럼 스타일의 다른 단어는 공백으로 바꿔도 됩니다. 2026-10-13처럼 네 자리 이상의 연도로 시작하는 숫자 세 개는 연, 월, 일로 읽는데, 로캘이 연도를 앞에 두고 다른 순서로 쓰면 그 순서를 따릅니다. 연도는 로캘의 달력과 상관없이 그레고리력 연도입니다. 한두 자리 연도는 올해에 가장 가까운 그런 해로 읽으므로, 50년쯤 떨어진 날짜는 네 자리를 모두 써야 하고, 세 자리 연도는 읽지 않습니다. locale이 없으면 둘 다 런타임의 로캘을 씁니다.

예시에서는 날짜가 아닌 텍스트가 날짜를 비우므로 폼은 날짜를 보내지 않고, 필드는 포커스를 잃을 때 유효하지 않음으로 표시됩니다. parseDate는 선택기의 min, max, isDateDisabled를 확인하지 않습니다. min과 max를 벗어난 입력 날짜는 폼을 유효하지 않게 하지만, isDateDisabled는 직접 확인하세요.

단독으로

단독으로 쓰면 Field.DatePicker가 필드의 컨트롤입니다. 날짜를 locale의 medium 스타일로, 날짜가 없으면 placeholder를 보여 주고, 필드의 다른 곳을 누르면 달력이 열립니다. Root에 name이 있으면 폼이 HTML이 쓰는 형식으로 날짜를 보냅니다. 예: 2026-10-13.

달력은 open과 onOpenChange, 또는 defaultOpen으로 열고 닫습니다. onOpenChange는 닫힌 이유를 받습니다. 날짜를 고르면 'select', 그 밖에는 'escape', 'outside', 'trigger', 필드가 비활성화되면 'disabled'입니다. 열리면 달력은 날짜에, 없으면 오늘이나 범위 안의 가장 가까운 날짜에 포커스를 둡니다. Escape를 누르면 닫히고, 포커스는 열리기 전에 있던 곳으로 돌아갑니다.

required는 날짜가 없는 폼을, min과 max는 그보다 이르거나 늦은 날짜가 있는 폼을 유효하지 않게 하며, 달력의 범위도 제한합니다. 브라우저는 선택기가 버튼 옆에 렌더링하는 네이티브 <input type="date">에서 세 가지를 모두 검사합니다. 이 입력은 화면과 보조 기술에서 숨겨지고 탭 정지점이 아니며, 폼이 필드의 name으로 보내는 값입니다. Form.Root 안에서는 문제가 필드의 ErrorMessage.Root에 페이지의 언어로 나타나고 min이나 max는 버튼이 날짜를 쓰는 방식으로 표기되며, 포커스는 숨은 입력이 아니라 버튼으로 갑니다. 요약의 해당 필드 행도 버튼으로 연결됩니다. Form.Root 밖에서도 브라우저가 제출을 막고, 포커스는 마찬가지로 버튼에 놓입니다. 네이티브 날짜 입력은 1년부터 275760년까지만 담을 수 있으므로, 그 밖의 값이나 범위는 날짜 없음으로 읽힙니다. 빈 값으로 전송되고 required, min, max는 이를 무시하며, 개발 빌드는 한 번 경고합니다. isDateDisabled는 검사에 포함되지 않습니다.

접근성을 지키는 조합

Todae가 맡는 것: 단독으로 쓰면 버튼은 Field.Root에서 id, aria-label, aria-labelledby, aria-describedby를 받고, Root나 바깥 Fieldset에서 네이티브 disabled와 aria-invalid를 받습니다. 레이블이 버튼의 이름이 되므로, 버튼에 보이는 날짜는 aria-describedby에 더해집니다. 텍스트 입력처럼 Field.ColorPicker, Field.DateRangePicker, Field.TimePicker, Field.DateTimePicker, 다른 Field.DatePicker가 아닌 Field 파트나 useField로 연결한 컨트롤이 필드에 함께 있으면 그 컨트롤이 대신 id와 aria-describedby를 받고, 버튼은 disabled와 aria-invalid를 유지하며, 자체 aria-label이 없으면 StringsProvider의 datePicker 문자열로 이름이 붙습니다. 버튼에는 aria-haspopup="dialog"와 aria-expanded가, 달력이 열려 있는 동안에는 aria-controls가 붙습니다. 팝오버는 datePicker 문자열로 이름이 붙은 role="dialog"이고, 안의 달력은 Calendar와 같은 키로 움직입니다. 글리프는 GlyphsProvider의 calendar입니다. 필수 선택기의 버튼은 버튼에 aria-required를 쓸 수 없으므로, StringsProvider의 required 문자열로 설명됩니다.

직접 제공할 것: 이름. htmlFor가 Root id인 Label이나, Root의 aria-label / aria-labelledby입니다. 텍스트 입력과 그 옆의 선택기에는 같은 날짜를 주고, formatDate로 쓰고 parseDate로 읽으세요.

주의할 점: 서버 렌더는 버튼 옆에 다른 컨트롤이 있는지 알 수 없으므로, 페이지가 하이드레이션될 때까지 버튼과 그 컨트롤 둘 다 필드의 id를 가지고 필드의 name으로 값을 보냅니다. value가 있으면 서버는 그 날짜를 서버의 로캘로 쓰므로, 서버에서 렌더링할 때는 locale을 넘기세요.

언제 쓰나요

Field.DatePicker

언제: 예약이나 마감일처럼 한 달을 보면서 고르는 편이 좋은 폼의 날짜.

쓰지 않을 때: 생일처럼 외우고 있는 날짜 → parseDate로 읽는 Field.TextInput. 페이지에 펼쳐 둔 달력 → Calendar. 시작일과 종료일 → Field.DateRangePicker. 시각이 있는 날짜 → Field.DateTimePicker.

formatDate

언제: 피커 옆 텍스트 입력처럼, 날짜를 로캘에 맞는 텍스트로 쓸 때.

쓰지 않을 때: 요일이나 월 이름이 있는 날짜 → Intl.DateTimeFormat.

parseDate

언제: 사람이 입력한 날짜를 읽을 때.

쓰지 않을 때: 2026-10-13처럼 형식이 정해진 텍스트 → Temporal.PlainDate.from.

Props

Field.DatePicker props
이름타입설명
value 필수Temporal.PlainDate | null

날짜, 없으면 null.

onValueChange(value: Temporal.PlainDate) => void

달력에서 고른 날짜를 받아 호출됩니다.

minTemporal.PlainDate

고를 수 있는 가장 이른 날짜. 이보다 이른 날짜가 있으면 폼이 유효하지 않습니다.

maxTemporal.PlainDate

고를 수 있는 가장 늦은 날짜. 이보다 늦은 날짜가 있으면 폼이 유효하지 않습니다.

requiredboolean

날짜가 없으면 폼이 유효하지 않습니다.

isDateDisabled(date: Temporal.PlainDate) => boolean

공휴일처럼 고를 수 없는 날짜에 true를 돌려줍니다.

firstDayOfWeek1 | 2 | 3 | 4 | 5 | 6 | 7

달력의 첫 열, 1(월요일)부터 7(일요일)까지. 없으면 브라우저가 알려 주는 로캘의 첫 요일, 그것도 없으면 7.

localestring

버튼의 날짜와 달력 이름의 로캘, 예: "ko". 없으면 브라우저의 로캘이므로, 서버에서 렌더링할 때는 넘기세요.

placeholderstring

showValue가 true일 때, 날짜가 없는 동안 버튼에 보입니다.

showValueboolean

버튼이 글리프 옆에 날짜나 placeholder를 보여 줄지 여부. 날짜를 보여 주는 텍스트 입력 옆에서는 false로 두세요.

기본값: true

openboolean

달력의 제어 열림 상태. onOpenChange와 함께 전달하세요.

defaultOpenboolean

비제어일 때 처음 열림 상태.

onOpenChange(open: boolean, reason: 'escape' | 'outside' | 'trigger' | 'select' | 'disabled') => void

달력이 열리거나 닫힐 때 호출되며, 닫힌 이유를 함께 받습니다.

formatDate props
이름타입설명
date 필수Temporal.PlainDate

쓸 날짜.

options.localestring

ko 같은 로캘. 지정하지 않으면 런타임의 로캘입니다.

parseDate props
이름타입설명
text 필수string

입력된 텍스트.

options.localestring

ko 같은 로캘. 지정하지 않으면 런타임의 로캘입니다.