# Date Input

## Usage

`DateInput` is a text field with a calendar button. People can type a date or open the calendar and pick a day.

Bind a date:

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { ref } from 'vue'

const date = ref(null)
</script>

<template>
  <DateInput v-model="date" />
</template>
```

The value is a local date. The field writes it back as a long date, such as March 21, 2026. It can also show a short date, a weekday, the system date, or custom text. A description can sit under the label, and validation can change the border or add a message under the field.

The calendar can open in a popover or a bottom sheet. The sheet shows one month. Picking a day writes it immediately, and Save closes the sheet. The month title opens month and year wheels. The system date control can be used all the time, or only on a touch device. When nothing is specified, a mouse gets the popover and a coarse pointer gets the system control. While a change is saving, the field cannot be edited. The save runs after the new value is written. The label can carry a hint and mark the field optional or required. The popover can show two months, and the week can start on a chosen day.

### Anatomy

- **Label**: Text above the input describing what date is expected.
- **Text input**: A field where the user can type a date directly. Parses common formats like MM/DD/YYYY.
- **Calendar icon**: A button that opens the calendar popover for visual date picking.
- **Calendar popover**: A month grid that appears when the icon is clicked or the input is focused.
- **Clear button**: A × button that resets the date value. Shown when clearable is set and a date is present.
- **Status message**: An error, warning, or success message below the input.

## Best practices

| Guideline | Practice |
| --- | --- |
| Do | Turn on `clearable` when the date is optional. |
| Do | Set `min` and `max` so people cannot pick a date the product will reject. |
| Do | Use a placeholder that shows the expected format. |
| Don't | Use it when the value needs a time of day. This field is a calendar date. |

## Examples

### Clearable

An optional event date with a clear button. The line above shows the selected value.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const value = ref<Date | null>(new Date(2026, 3, 6))

function iso(date: Date | null): string {
  if (!date)
    return ''
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${date.getFullYear()}-${month}-${day}`
}
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      {{ value ? `Selected: ${iso(value)}` : 'No date selected' }}
    </Text>
    <DateInput
      v-model="value"
      label="Event date"
      description="Pick a date for your event"
      placeholder="Select a date"
      clearable
    />
  </VStack>
</template>
```

### Description

Helper text under the label explains what the date is for.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const value = ref<Date | null>(null)
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      Helper text explains what the field expects
    </Text>
    <DateInput
      v-model="value"
      label="Start date"
      description="Your subscription begins on this date"
      placeholder="Select a start date"
    />
  </VStack>
</template>
```

### Formats

The same committed date as a short month, a long month, a weekday, an ISO date, or a custom string.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const value = ref<Date | null>(new Date(2026, 2, 21))

function shipBy(iso: string): string {
  return `Ship by ${iso}`
}
</script>

<template>
  <VStack :gap="4" style="width: 320px; max-width: 100%">
    <Text type="supporting" color="secondary">
      The same committed date, displayed with different formats.
    </Text>
    <DateInput v-model="value" label="Short month (date)" format="date" />
    <DateInput v-model="value" label="Long month (date_long, default)" format="date_long" />
    <DateInput v-model="value" label="With weekday (date_weekday)" format="date_weekday" />
    <DateInput v-model="value" label="ISO 8601 (system_date)" format="system_date" />
    <DateInput v-model="value" label="Custom function" :format="shipBy" />
  </VStack>
</template>
```

### Min and max

Only the 8th through the 21st of the current month can be selected. A coarse pointer opens a bottom sheet.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
import { ref } from 'vue'

const today = new Date()
const year = today.getFullYear()
const month = today.getMonth()
const startDay = 8
const endDay = 21
const min = new Date(year, month, startDay)
const max = new Date(year, month, endDay)
const monthName = new Date(year, month, 1).toLocaleString('en-US', { month: 'short' })
const description = `Available dates: ${monthName} ${startDay} – ${endDay}, ${year}`
const value = ref<Date | null>(null)

function iso(date: Date | null): string {
  if (!date)
    return ''
  const monthNumber = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  return `${date.getFullYear()}-${monthNumber}-${day}`
}
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <Text type="supporting" color="secondary">
      {{ value ? `Booked: ${iso(value)}` : 'Pick a date in the available range' }}
    </Text>
    <DateInput
      v-model="value"
      label="Booking date"
      :min="min"
      :max="max"
      :description="description"
      placeholder="Select a booking date"
      presentation="adaptive-bottom-sheet"
    />
  </VStack>
</template>
```

### Validation

Error, warning, and success, each with a message.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { ref } from 'vue'

const errorVal = ref<Date | null>(new Date(2026, 0, 25))
const warningVal = ref<Date | null>(new Date(2026, 11, 25))
const successVal = ref<Date | null>(new Date(2026, 2, 10))
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <DateInput
      v-model="errorVal"
      label="Event date"
      :status="{ type: 'error', message: 'This date is already booked' }"
    />
    <DateInput
      v-model="warningVal"
      label="Preferred date"
      :status="{ type: 'warning', message: 'This date falls on a holiday' }"
    />
    <DateInput
      v-model="successVal"
      label="Start date"
      :status="{ type: 'success', message: 'Date confirmed' }"
    />
  </VStack>
</template>
```

### Saving and labels

The date is written first, and the field stays busy until the save finishes. Weekends cannot be picked. The other field is marked optional.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { VStack } from '@astryx-vue/core/Stack'
import { ref } from 'vue'

const ship = ref<Date | null>(new Date(2026, 2, 21))
const due = ref<Date | null>(null)

function save(value: Date | null): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, value ? 900 : 400)
  })
}
</script>

<template>
  <VStack :gap="4" style="width: 240px; max-width: 100%">
    <DateInput
      v-model="ship"
      label="Ship date"
      description="Saved when the date changes."
      label-tooltip="Warehouse days are Monday to Friday."
      is-required
      clearable
      :change-action="save"
      :date-constraints="[(date: Date) => date.getDay() !== 0 && date.getDay() !== 6]"
    />
    <DateInput
      v-model="due"
      label="Follow-up"
      is-optional
      placeholder="Select a date"
    />
  </VStack>
</template>
```

### System date

The browser date control, with the committed day still shown as a long date.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { ref } from 'vue'

const date = ref<Date | null>(new Date(2026, 2, 21))
</script>

<template>
  <div style="width: 240px; max-width: 100%">
    <DateInput
      v-model="date"
      label="System date"
      description="This field opens the browser date control."
      presentation="native"
      clearable
    />
  </div>
</template>
```

### Bottom sheet

A bottom sheet on both mouse and touch. Picking a day writes it, and Save closes the sheet.

```vue
<script setup lang="ts">
import { DateInput } from '@astryx-vue/core/DateInput'
import { ref } from 'vue'

const date = ref<Date | null>(new Date(2026, 2, 21))
</script>

<template>
  <div style="width: 240px; max-width: 100%">
    <DateInput
      v-model="date"
      label="Due"
      description="Opens a one-month sheet. Save closes it."
      presentation="bottom-sheet"
    />
  </div>
</template>
```

## DateInput Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `Date \| null` | `null` | The selected date. |
| `placeholder` | `string` | `Select a date` / `选择日期` | Placeholder text. English by default; Chinese when `zh-CN` is provided. |
| `min` | `Date` | — | Earliest selectable date. |
| `max` | `Date` | — | Latest selectable date. |
| `isDateDisabled` | `(date: Date) => boolean` | — | Return true to disable a date. |
| `dateConstraints` | `Array<(date: Date) => boolean>` | — | A day is disabled when any function returns false. |
| `name` | `string` | — | Form field name. |
| `label` | `string` | — | Field label. |
| `description` | `string` | — | Helper text under the label. |
| `isLabelHidden` | `boolean` | `false` | Keeps the label for screen readers and hides it visually. `labelHidden` is the older name; either one hides it. |
| `isOptional` | `boolean` | — | Marks the label as optional. |
| `isRequired` | `boolean` | — | Marks the field required and sets aria-required on the input. |
| `labelTooltip` | `string` | — | Hint shown from the info icon at the end of the label. |
| `format` | `'date' \| 'date_long' \| 'date_weekday' \| 'system_date' \| ((iso: string) => string)` | `'date_long'` | How a committed date is displayed. A function receives `YYYY-MM-DD`. |
| `presentation` | `'popover' \| 'bottom-sheet' \| 'native' \| 'adaptive-bottom-sheet' \| 'adaptive-native'` | adaptive-native when omitted | Popover, bottom sheet, system date control, or a switch between them based on the pointer. presentation wins when nativePicker is also set. |
| `nativePicker` | `'touch' \| 'always' \| 'never'` | — | Older surface name. touch follows the pointer, always uses the system control, and never switches between the popover and the bottom sheet by pointer. presentation wins when both are set. |
| `numberOfMonths` | `1 \| 2` | `1` | Months shown side by side in the calendar. The system date control cannot express this. |
| `weekStartsOn` | `0-6 \| 'sun' \| 'mon' \| ...` | — | First day of the week in the calendar. The system date control cannot express this. |
| `disabled` | `boolean` | `false` | Disables the field. |
| `isLoading` | `boolean` | `false` | The field cannot be edited and shows a loading indicator. |
| `changeAction` | `(value: Date \| null) => void \| Promise<void>` | — | Runs after the value is committed. A promise keeps the field busy until it settles. |
| `width` | `number \| string` | — | Width of the whole field. A number is pixels. |
| `disabledMessage` | `string` | — | Explains why the field is disabled. With `disabled`, the message shows in a tooltip on hover or keyboard focus and the input stays focusable with `aria-disabled`; the value still cannot change. The older `disabledReason` name works too. |
| `hasClear` | `boolean` | `false` | Shows a clear button when a date is set. Clearing also returns focus to the input. The older `clearable` name works too. |
| `clearLabel` | `string` | `Clear` / `清除` | Accessible name for the clear button. Includes `label` when one is set. English by default; Chinese when `zh-CN` is provided. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Field size. |
| `status` | `'default' \| 'error' \| 'warning' \| 'success' \| { type, message? }` | — | Validation status. An object with `message` shows that text under the field. |
| `statusVariant` | `'attached' \| 'detached' \| 'tooltip'` | `'attached'` | Where the status message sits. |
| `containerClass` | `string \| object \| array` | — | Class for the field container. Has no effect, and warns in development, inside InputGroup and in a horizontal-labels FormLayout. |
| `containerStyle` | `StyleValue` | — | Inline style for the field container. Same targeting as containerClass. |
| `controlClass` | `string \| object \| array` | — | Class for the bordered control box. A class written on the component lands here too, not on the inner input. |
| `controlStyle` | `StyleValue` | — | Inline style for the bordered control box. |

## DateInput events

| Name | Payload | Description |
| --- | --- | --- |
| `update:modelValue` | `value: Date \| null` | Emitted when the date changes. |
| `change` | `value: Date \| null` | Emitted when the date changes. |

## DateInput slots

| Name | Description |
| --- | --- |
| `prefix` | Icon shown before the input. |
| `suffix` | Content shown after the input. |
