# Date Input
날짜를 연·월·일 세그먼트로 나눠 키보드로 타이핑해 넣는 입력 필드입니다.
> 원본: `03_ASSET_GUIDE/catalog/date-input.js` · CSS: `03_ASSET_GUIDE/styles/elements/date-input.css`
> kind `component` · category `action-input` · status `stable` · specSource `measured`
> [!CAUTION]
> **Date Picker 가 아닙니다**
> 카탈로그에는 달력 격자에서 날짜를 고르는 Date Picker 가 **별개 항목으로** 있습니다.
> Date Input 은 그 앞단의 입력 필드이고, 주 입력 수단은 키보드입니다. 오른쪽 끝의 트리거는
> Date Picker 를 여는 자리이지만 **이 저장소에는 그 로직이 없습니다.**
> [!NOTE]
> **접근성 계약의 근거**
> 세그먼트를 각각 `role="spinbutton"` 으로 두는 구조와 화살표 키 동작은
> [WAI-ARIA APG 의 spinbutton 패턴](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/)
> 을 근거로 적었습니다. 아래 수치는 `styles/elements/date-input.css` 와
> `styles/tokens.css` 에서 실측한 값입니다.
---
## 1. 구성
하나의 **Field** 안에 연·월·일 **Segment** 가 **Separator** 로 이어져 있고, 오른쪽 끝에
달력을 여는 **Trigger** 가 선택적으로 붙습니다. 제출되는 값은 눈에 보이지 않는 **Value** 가
들고 있습니다.
```
┌──────────────────────────────────────────┐
│ [1994] . [ MM ] . [ DD ] [📅] │
└──────────────────────────────────────────┘
① ②③ ④ ⑤ ⑥
```
| # | 요소 | 필수 | 클래스 | 설명 |
|---|---|---|---|---|
| ① | Field | 필수 | `atom-date-input` | 상자를 그리고 `:focus-within` 링을 소유합니다. 높이는 [Text Input](./text-input.md) 과 같습니다. |
| ② | Segments | 필수 | `atom-date-input__segments` | `role="group"` 입니다. 필드의 이름(예: 생년월일)은 여기에 붙습니다. |
| ③ | Segment | 필수 | `atom-date-input__segment` | `YYYY` · `MM` · `DD` 각각 하나. `role="spinbutton"` 이고 자기 초점과 자기 값을 가집니다. |
| ④ | Separator | 필수 | `atom-date-input__separator` | `.` · `-` · `/` 글리프입니다. 초점을 받지 않는 장식이고 `format` 이 정합니다. |
| — | Dash | 선택 | `atom-date-input__dash` | 범위 입력에서 두 그룹을 잇는 `~` 입니다. 선택 사항입니다. |
| ⑤ | Trigger | 선택 | `atom-date-input__trigger` | 달력 아이콘 버튼입니다. Date Picker 를 여는 자리이고 선택 사항입니다. |
| ⑥ | Value | 필수 | `atom-date-input__value` | 화면에서 감춘 native `input` 입니다. 폼 제출과 `required` · `disabled` · `readonly` 를 이쪽이 맡습니다. |
> [!NOTE]
> **세그먼트를 쪼개는 이유**
> 마스킹한 `input` 하나로는 스크린리더에 캐럿 위치까지만 알릴 수 있습니다. 세그먼트를 각각
> `role="spinbutton"` 으로 두면 "월, 8, 1에서 12 사이" 처럼 칸의 종류와 범위가 읽힙니다.
이름과 클래스는 이렇게 짝지어집니다.
| 이 문서 | 클래스 | 역할 |
| --- | --- | --- |
| Field | `atom-date-input` | 상자 · 초점 링 · 상태 |
| Segments | `atom-date-input__segments` | `role="group"`. 필드 이름이 붙는 곳 |
| Segment | `atom-date-input__segment` | `role="spinbutton"`. 연 · 월 · 일 |
| Separator | `atom-date-input__separator` | `.` `-` `/` 글리프 |
| Dash | `atom-date-input__dash` | 범위의 `~` |
| Trigger | `atom-date-input__trigger` | 달력 버튼 |
| Value | `atom-date-input__value` | 감춘 native input |
---
## 2. 종류 / 크기
### Format
`format` 은 **세그먼트의 순서**와 **구분자 글리프**를 함께 정합니다.
구분자는 별도 요소이므로 순서를 바꿔도 세그먼트의 역할(`aria-label`)은 그대로 따라갑니다.
| format | 표기 | 세그먼트 순서 | 주 사용처 |
| --- | --- | --- | --- |
| `YYYY-MM-DD` | 2026-08-27 | 연 → 월 → 일 | ISO. 서버 값을 그대로 보여줄 때 |
| `YYYY.MM.DD` | 2026.08.27 | 연 → 월 → 일 | 국내 화면 기본 |
| `DD/MM/YYYY` | 27/08/2026 | 일 → 월 → 연 | 유럽권 로케일 |
> [!WARNING]
> **순서는 로케일에서 가져오고, 값은 ISO 로 저장합니다**
> 같은 `08/27` 이 로케일에 따라 8월 27일이거나 27월(없는 달)입니다. 화면 순서는 로케일이
> 정하되 `value` 는 항상 `YYYY-MM-DD` 로 주고받으세요. 화면 표기와 저장 값을 같은 문자열로
> 쓰면 로케일이 바뀔 때 과거 데이터의 뜻이 달라집니다.
### Size
높이 축은 [Text Input](./text-input.md) 과 **같은 값**을 씁니다. 같은 폼에
나란히 놓이므로 높이가 달라지면 안 됩니다.
| Size | 높이 | 좌우 여백 | 글자 | 트리거 | 주 사용처 |
| --- | --- | --- | --- | --- | --- |
| `small` | 40px | 12px | 14px | 24px | 표 헤더·필터 줄 |
| `medium` | 48px | 16px | 15px | 28px | 기본 폼 |
| `large` | 56px | 20px | 16px | 32px | 입력 하나만 있는 화면 |
세그먼트 숫자에는 `font-variant-numeric: tabular-nums` 를 걸어 둡니다. 비례 숫자를 쓰면
`11` 이 `08` 보다 좁아서 타이핑하는 동안 구분자가 좌우로 흔들립니다.
### Trigger
오른쪽 끝의 달력 버튼입니다. 누르면 Date Picker 를 여는 자리이고, **이 저장소에는 그 로직이
없습니다.** 트리거는 보조 경로이므로, 트리거 없이 타이핑만 하는 형태도 그대로 동작해야
합니다.
> [!NOTE]
> **트리거는 세그먼트 뒤에 옵니다**
> Tab 순서는 세그먼트 그룹 → 트리거입니다. 트리거가 먼저 오면 키보드 사용자가 필드보다
> 오버레이 버튼에 먼저 닿습니다.
### Calendar popover
Trigger 를 누르면 달력이 열립니다. shadcn/ui 의 Date Picker 와 같은 **Popover 안에
Calendar** 구성입니다. shadcn/ui 와 달리 여기서는 타이핑이 주 경로이고 달력은 보조 경로입니다.
팝오버가 열려 있어도 세그먼트는 그대로 편집됩니다.
| 부분 | 역할 |
| --- | --- |
| `__popover` | 필드 아래에 뜨는 판. `role="dialog"` |
| `__caption` | 월·연 라벨과 좌우 이동 버튼 |
| `__weekdays` | 요일 머리글 행 |
| `__grid` | 한 달의 날짜 표. 실제 `
` |
| `__day` | 날짜 한 칸. 선택·오늘·이웃달·비활성 상태를 가짐 |
| `__footer` | 프리셋이나 지우기. 선택 사항 |
> [!NOTE]
> **월 이동 버튼은 양 끝에 둡니다**
> 라벨 옆에 화살표를 붙이면 달 이름의 폭에 따라 버튼 위치가 매달 바뀝니다. 양 끝에 두면
> 연속으로 눌러도 위치가 고정됩니다.
날짜 칸의 상태는 shadcn 이 감싸는 react-day-picker 의 이름을 그대로 씁니다.
| 상태 | 속성 | 표현 |
| --- | --- | --- |
| 선택됨 | `aria-selected="true"` | solid 채움 |
| 오늘 | `data-today` | 테두리 링 + 굵게 |
| 이웃 달 | `data-outside` | 흐리게. 자리는 유지 |
| 비활성 | `aria-disabled="true"` | 흐리게 + 커서 금지 |
| 범위 | `data-range_start / _middle / _end` | 양 끝만 채우고 가운데는 약한 띠 |
> [!NOTE]
> **오늘은 채우지 않고 링으로 표시합니다**
> 오늘이 선택된 날일 수 있습니다. 둘 다 채움으로 그리면 오늘 표시가 가려집니다. 링과 채움은
> 같은 칸에서 겹쳐도 둘 다 보입니다.
`--range` 필드에서는 달력도 두 달을 나란히 폅니다. 시작과 끝이 다른 달인 경우가 많기
때문입니다.
> [!WARNING]
> **팝오버는 화면 밖으로 열리면 안 됩니다**
> 이 파일은 필드 바로 아래에 붙입니다. 실제 구현은 트리거의 위치를 재서 아래 공간이 모자라면
> 위로 뒤집고, 오른쪽이 모자라면 `--align_end` 로 끝을 맞춰야 합니다.
### Range
`range` 는 시작일과 종료일을 **한 상자 안에** 두고 `~` 로 잇습니다. 상자가 둘이면 타이핑
중인 쪽에만 초점 링이 걸려 두 값이 한 값이라는 것이 드러나지 않습니다.
| 대상 | 무엇을 붙이는가 |
| --- | --- |
| `atom-date-input--range` | 상자 하나에 세그먼트 그룹 둘 |
| `role="group"` | 그룹마다 하나. 이름은 시작일 · 종료일 |
| `atom-date-input__dash` | 두 그룹 사이의 `~`. `aria-hidden` |
> [!WARNING]
> **시작이 종료보다 늦으면 어느 쪽이 틀린 것인지 밝히세요**
> 상자가 하나라서 `[data-invalid]` 를 걸면 둘 다 붉어집니다. 오류 문장에서 "종료일이
> 시작일보다 앞섭니다" 처럼 대상을 지목하세요.
### 클래스 계약
`styles/elements/date-input.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다.
| Prop · 축 | 값 | 클래스 |
|---|---|---|
| `size` | `small` · `medium` · `large` | `--size_small` · — · `--size_large` |
| `range` | `false` · `true` | — · `--range` |
| (focused) | 정적 대역 | `--focused` (`:focus-within` 과 같은 모양) |
| (empty) | 양쪽 세그먼트가 빈 구분자 | `__separator--empty` |
| `numberOfMonths` | `1` · `2` | — · `__calendar--months_2` |
| (align) | 팝오버 끝 맞춤 | `__popover--align_end` |
> [!NOTE]
> `__separator--empty` 는 CSS 에 정의되어 있지만 헤더 계약 주석에는 없습니다(원본 registry `css-defined-not-declared`).
상태 선택자: `:hover` · `:focus-within` · `[data-invalid]` · `[data-disabled]` · `[data-readonly]` · `:has(.atom-date-input__value[disabled])`
세그먼트: `[data-empty]` · `[data-focused]` · `:focus` · `:focus-visible`
트리거: `[aria-expanded="true"]` · 팝오버: `[data-state="open|closed"]`
날짜: `[aria-selected]` · `[data-today]` · `[data-outside]` · `[aria-disabled]` · `[disabled]` · `[data-range_start|_middle|_end]` · 월 이동: `[aria-disabled="true"]` · `[disabled]`
---
## 3. 상태
필드 전체의 초점과 세그먼트 하나의 초점은 **다른 표시**를 씁니다. 필드는 2px 링, 초점
세그먼트는 채워진 칩입니다. 둘 다 링이면 화살표 키가 바꿀 칸을 구분할 수 없습니다.
| 상태 | 표시 | 붙는 자리 |
| --- | --- | --- |
| 비었음 | 세그먼트가 `YYYY` `MM` `DD` 를 subtle 색으로 보여줍니다 | `[data-empty]` |
| 입력 중 | 필드에 2px 링 + 초점 세그먼트가 채워진 칩 | `--focused` |
| 채워짐 | 세그먼트가 neutral 색 숫자 | 기본 |
| 오류 | 필드 링이 critical 색으로 바뀝니다 | `[data-invalid]` |
| 읽기 전용 | weak 배경, 링 없음, muted 글자 | `[data-readonly]` |
| 비활성 | 45% 불투명도 + 포인터 차단 | `[data-disabled]` |
세그먼트 쪽 초점은 `[data-focused]` 와 `:focus` 를 같은 규칙으로 묶어 둡니다.
`[data-focused]` 는 정적 데모용 대역이고, 실제 화면에서는 `:focus` 가 같은 모양을 그립니다.
> [!NOTE]
> 구현은 `--focused` · `[data-focused]` 를 붙이지 않고 실제 `:focus-within` · `:focus` 에 맡깁니다.
> `[data-invalid]` 는 `invalid` prop · Field 의 오류 외에, 세 세그먼트가 모두 채워졌는데 존재하지 않는 날짜이거나
> `min`/`max` 를 벗어났을 때, range 에서 시작이 종료보다 늦을 때 자동으로 붙습니다.
---
## 4. Props
| Name | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `value` | `string \| null` | `null` | | 선택된 날짜입니다. 화면 표기와 무관하게 항상 `YYYY-MM-DD` 로 주고받습니다. |
| `format` | `"YYYY-MM-DD" \| "YYYY.MM.DD" \| "DD/MM/YYYY"` | `"YYYY.MM.DD"` | | 세그먼트 순서와 구분자 글리프를 정합니다. 로케일에서 뽑는 것을 권합니다. |
| `showCalendar` | `boolean` | `true` | | Trigger 와 Calendar popover 를 답니다. `false` 면 타이핑 전용입니다. |
| `numberOfMonths` | `number` | `1` | | 팝오버에 나란히 펼 달의 수입니다. `range` 에서는 2가 기본입니다. |
| `showOutsideDays` | `boolean` | `true` | | 이웃 달의 날짜를 흐리게 남겨 주를 사각형으로 유지합니다. |
| `disabledDates` | `(date) => boolean` | | | 고를 수 없는 날짜를 정합니다. |
| `size` | `"small" \| "medium" \| "large"` | `"medium"` | | 높이와 좌우 여백입니다. Text Input 과 같은 값입니다. |
| `range` | `boolean` | `false` | | 한 상자에 시작 · 종료 두 세그먼트 그룹을 둡니다. |
| `withTrigger` | `boolean` | `true` | | 오른쪽 끝에 달력 버튼을 그립니다. `false` 면 타이핑만 받습니다. |
| `min` | `string` | | | 허용 최소 날짜입니다. 세그먼트의 `aria-valuemin` 계산에도 쓰입니다. |
| `max` | `string` | | | 허용 최대 날짜입니다. |
| `name` | `string` | | | 감춘 `input` 의 `name` 입니다. 폼 제출 키입니다. |
| `required` | `boolean` | `false` | | 필수 여부입니다. 별표 표시는 Field 가 그립니다. |
| `invalid` | `boolean` | `false` | | `[data-invalid]` 를 붙입니다. 오류 문장은 Field 에 함께 두세요. |
| `readOnly` | `boolean` | `false` | | 값은 읽히지만 바꿀 수 없습니다. `[data-readonly]` 로 나갑니다. |
| `disabled` | `boolean` | `false` | | `[data-disabled]` 를 붙이고 세그먼트 · 트리거의 포인터를 막습니다. |
| `onChange` | `(value: string \| null) => void` | | | 세 세그먼트가 모두 채워져 유효한 날짜가 됐을 때 호출됩니다. |
| `onSegmentChange` | `(segment, value) => void` | | | 세그먼트 하나가 바뀔 때마다 호출됩니다. 부분 입력을 추적할 때 씁니다. |
| `onTriggerClick` | `() => void` | | | 달력 버튼을 눌렀을 때입니다. Date Picker 를 여는 것은 이 콜백을 받는 쪽의 일입니다. |
구현(`@atom/ui`)에서 더하거나 정한 것:
| Prop | Type | 설명 |
| --- | --- | --- |
| `defaultValue` | `string \| null` | 비제어로 쓸 때의 처음 값. `value` 를 주지 않으면 비제어입니다. |
| `aria-label` · `aria-labelledby` | `string` | 필드 이름. Root 가 아니라 `role="group"`(Segments)에 붙습니다. Field 안이면 Field 의 라벨이 연결됩니다. range 에서는 Root 가 `role="group"` 이 되어 이름을 받고, 두 그룹은 시작일 · 종료일입니다. |
| `value` (range) | `"YYYY-MM-DD/YYYY-MM-DD"` | range 값은 ISO 8601 구간 표기로 주고받습니다. 두 끝이 모두 완성돼야 값이 됩니다. |
| `onSegmentChange` | `(segment: "year" \| "month" \| "day", value: number \| null, part: "start" \| "end") => void` | 세 번째 인자로 range 의 어느 쪽인지 알려줍니다. |
| `children` | `ReactNode` | Calendar popover 아래에 붙는 내용(``). |
### Parts (compound)
| Part | 요소 | 설명 |
|---|---|---|
| `DateInput` | `
` | Field. 위 Props 를 받습니다. Segments · Segment · Separator · Dash · Value · Trigger · Calendar popover 를 모두 그립니다. |
| `DateInput.Footer` | `