---
title: Date Field
description: A form input for selecting dates with calendar picker, validation, and automatic range correction.
---

```tsx
'use client';

import { DateField } from '@/ui/forms/DateField';

export function DateFieldDemo() {
  return (
    <DateField
      label="Select a date"
      description="Choose your preferred date"
      openDatepicker="Open date picker"
    />
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/date-field.json
```

Or manually, copy the following code into `/ui/forms/date-field.tsx`:

```tsx
import clsx from 'clsx';
import { differenceInMilliseconds, formatISO, isValid, parseISO } from 'date-fns';
import { type ChangeEvent, useCallback, useEffect, useId, useRef, useState } from 'react';
import { Icon, type IconicNames, type IconicTypes } from '@clubmed/trident-icons';

import { useValue } from '../hooks/useValue';
import { useInternalStatus } from '../hooks/useInternalStatus';
import { FormControl, type FormControlProps } from './FormControl';

interface DateFieldProps extends Omit<FormControlProps<Date | null>, 'max' | 'min'> {
  description?: string;
  /**
   * Open datepicker text
   */
  openDatepicker?: string;
  id?: string;
  icon?: IconicNames;
  iconType?: IconicTypes;
  errorMessage?: string;
  dataTestId?: string;
  /**
   * Minimum Date allowed
   */
  min?: Date | undefined;
  /**
   * Maximum Date allowed
   */
  max?: Date | undefined;
}

const formatDate = (value: Date | number | string | undefined | null): string => {
  if (!value) {
    return '';
  }

  // Already an ISO date string (YYYY-MM-DD) coming back through the useValue
  // formatter chain — return it as-is, otherwise `new Date(isoString)` would
  // parse it as UTC midnight and `formatISO(..., 'date')` (local time) would
  // shift the calendar day in negative-UTC timezones.
  if (typeof value === 'string') {
    return value;
  }

  if (!isValid(new Date(value))) {
    return '';
  }

  return formatISO(new Date(value), { representation: 'date' });
};

const validateDateRange = (date: string, min?: Date, max?: Date): boolean => {
  if (!date) {
    return true;
  }

  const normalizeDate = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
  // parseISO interprets a bare YYYY-MM-DD as local midnight, while `new Date(date)`
  // would interpret it as UTC midnight and silently shift the calendar day in
  // negative-UTC timezones.
  const inputDate = parseISO(date);
  const normalizedInputDate = normalizeDate(inputDate);

  if (min && normalizedInputDate < normalizeDate(min)) {
    return false;
  }
  if (max && normalizedInputDate > normalizeDate(max)) {
    return false;
  }

  return true;
};

export const DateField = (props: DateFieldProps) => {
  const internalId = useId();

  const {
    id = internalId,
    name = id,
    label,
    description,
    openDatepicker,
    validationStatus = 'default',
    icon = 'CalendarDefault',
    iconType,
    errorMessage,
    disabled = false,
    required = false,
    hideRequiredStar,
    dataTestId = 'DateField',
    value: initialValue,
    min: initialMin,
    max: initialMax,
    onChange,
    className,
    ...rest
  } = props;

  // NO LOGIC IS IMPLEMENTED YET HERE
  // THIS IS A VISUAL COMPONENT ONLY
  const ref = useRef<HTMLInputElement>(null);
  const [isFocused, setIsFocused] = useState<boolean>(false);

  const internalStatus = useInternalStatus({
    isDisabled: disabled,
    validationStatus,
  });

  const { setValue, value } = useValue<string, Date | number | undefined | null>({
    formatter: formatDate as never,
    initialValue,
    onChange: (event, fieldValue) => onChange?.(event, fieldValue ? parseISO(fieldValue) : null),
  });

  const min = formatDate(initialMin);
  const max = formatDate(initialMax);

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    setValue(event.target.value, event.nativeEvent);
  };

  const handleClickPicker = () => {
    ref.current?.showPicker();
  };

  const handleOutOfRangeDate = useCallback(() => {
    const currentDate = parseISO(value);

    if (initialMin && initialMax) {
      const distanceToMin = Math.abs(differenceInMilliseconds(currentDate, initialMin));
      const distanceToMax = Math.abs(differenceInMilliseconds(currentDate, initialMax));
      setValue(distanceToMin <= distanceToMax ? min : max);
      return;
    }

    if (initialMin) {
      setValue(min);
      return;
    }

    if (initialMax) {
      setValue(max);
      return;
    }
  }, [initialMax, initialMin, max, min, setValue, value]);

  useEffect(() => {
    const isValid = validateDateRange(value, initialMin, initialMax);
    if (!isValid && !isFocused) {
      handleOutOfRangeDate();
    }
  }, [handleOutOfRangeDate, initialMax, initialMin, isFocused, value]);

  return (
    <FormControl
      id={id}
      label={label}
      description={description}
      dataName="DateField"
      dataTestId={dataTestId}
      disabled={disabled}
      required={required}
      hideRequiredStar={hideRequiredStar}
      validationStatus={validationStatus}
      errorMessage={errorMessage}
      className={className}
    >
      <div className="relative" role="group">
        <input
          {...rest}
          type="date"
          className={clsx(
            'text-b3 rounded-pill date-field min-h-[50px] w-full inset-ring px-20 py-12 ps-[52px] font-normal outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',
            {
              'inset-ring-middleGrey focus:inset-ring-black active:inset-ring-black':
                internalStatus === 'default',
              'pe-[52px]': internalStatus === 'error' || internalStatus === 'success',
              'bg-pearl inset-ring-middleGrey': disabled,
              'bg-white text-black': !disabled,
              'inset-ring-red': !disabled && internalStatus === 'error',
              'inset-ring-green': !disabled && internalStatus === 'success',
            },
          )}
          id={id}
          name={name}
          disabled={disabled}
          required={required}
          ref={ref}
          value={value}
          min={min}
          max={max}
          onChange={handleChange}
          onFocus={() => setIsFocused(true)}
          onBlur={() => setIsFocused(false)}
        />
        <div
          className={clsx(
            'pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12',
            {
              'text-grey': disabled,
              'text-red': !disabled && internalStatus === 'error',
              'text-green': !disabled && internalStatus === 'success',
            },
          )}
        >
          <button
            className="pointer-events-auto leading-none outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2"
            onClick={handleClickPicker}
          >
            <Icon name={icon} iconType={iconType} width="24px" />
            <span className="sr-only">
              {openDatepicker}
              {/* REPLACE THAT WITH SOMETHING LIKE "OPEN DATEPICKER" SOONER RATHER THAN LATER PLZ */}
            </span>
          </button>
          <span className="ms-auto flex gap-x-8">
            {internalStatus === 'error' && <Icon name="CrossDefault" width="24px" />}
            {internalStatus === 'success' && <Icon name="CheckDefault" width="24px" />}
          </span>
        </div>
      </div>
    </FormControl>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { DateField } from '@/docs/lib/ui/forms/date-field';

export function Component() {
  const [date, setDate] = useState<Date | null>(null);

  return (
    <DateField
      label="Departure Date"
      description="Select your preferred departure date"
      openDatepicker="Open calendar picker"
      value={date}
      onChange={(_, value) => setDate(value)}
      min={new Date()}
      required
    />
  );
}
```

## API Reference

### DateField

| Prop               | Type                                  | Default             | Description                                                                                                                        |
| ------------------ | ------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `label`            | `string`                              | `undefined`         | The label text displayed above the input field.                                                                                    |
| `description`      | `string`                              | `undefined`         | Helper text displayed below the label to provide additional context.                                                               |
| `openDatepicker`   | `string`                              | `undefined`         | Accessibility text for the calendar button, announced to screen readers. Should describe the action, e.g., "Open calendar picker". |
| `id`               | `string`                              | Auto-generated      | The unique identifier for the input field. Auto-generated if not provided.                                                         |
| `name`             | `string`                              | Same as `id`        | The name attribute for the input field, used in form submissions. Defaults to the `id` value.                                      |
| `value`            | `Date \| null`                        | `undefined`         | The current date value. Pass `null` or `undefined` for no selection.                                                               |
| `onChange`         | `(event: Event, value: Date \| null) => void` | `undefined` | Callback called when date changes. Receives browser event, new Date value (or `null` when cleared). |
| `min`              | `Date`                                | `undefined`         | The minimum allowed date. Dates below this will auto-correct on blur.                                                              |
| `max`              | `Date`                                | `undefined`         | The maximum allowed date. Dates above this will auto-correct on blur.                                                              |
| `icon`             | `IconicNames`                         | `"CalendarDefault"` | The icon name from @clubmed/trident-icons to display in the calendar button.                                                       |
| `iconType`         | `IconicTypes`                         | `undefined`         | Optional icon type variant for the calendar icon.                                                                                  |
| `disabled`         | `boolean`                             | `false`             | Whether the field is disabled. Disabled fields have reduced opacity and cannot be interacted with.                                 |
| `required`         | `boolean`                             | `false`             | Whether the field is required. Displays a red asterisk next to the label.                                                          |
| `hideRequiredStar` | `boolean`                             | `false`             | When `true`, hides the required asterisk even if the field is required.                                                            |
| `validationStatus` | `"default" \| "error" \| "success"`   | `"default"`         | The validation state of the field. Changes border color and shows status icons.                                                    |
| `errorMessage`     | `string`                              | `undefined`         | Error message text displayed when `validationStatus` is `"error"`.                                                                 |
| `dataTestId`       | `string`                              | `"DateField"`       | Custom test ID for the component wrapper, useful for testing.                                                                      |
| `className`        | `string`                              | `undefined`         | Additional CSS classes for the input element.                                                                                      |

The component also accepts all standard HTML input attributes via spreading (`...rest`).

## Notes

### Date Picker Integration

The component uses the native HTML5 date input with enhanced styling. Clicking the calendar icon button triggers the browser's native date picker via the `showPicker()` API.

### Automatic Range Correction

When `min` and/or `max` props are provided, the component automatically corrects out-of-range dates when the input loses focus (blur event). The correction behavior:

- If only `min` is set: corrects to `min` if value is below
- If only `max` is set: corrects to `max` if value is above
- If both are set: corrects to the closest boundary date
- No correction occurs while the input is focused, allowing users to type freely

### Validation States

The component supports three validation states via the `validationStatus` prop:

- `"default"`: Normal state with light gray border
- `"error"`: Red border with cross icon, displays `errorMessage` if provided
- `"success"`: Green border with check icon

### Form Control Integration

DateField wraps the input in a `FormControl` component that provides consistent styling for labels, descriptions, error messages, and validation states across all form fields.

### Accessibility

- The calendar button includes a screen reader label via `openDatepicker` prop
- The component automatically associates the label with the input using the `id` attribute
- Disabled fields have appropriate visual feedback with reduced opacity
- Required fields are indicated with a red asterisk (unless `hideRequiredStar` is true)
- Status icons (check/cross) provide visual validation feedback

### Client Component

This component uses React hooks (`useState`, `useEffect`, `useRef`) and must be used in a client component. Add `"use client"` at the top of files that use this component.

### Date Format

The component internally uses ISO 8601 date format (YYYY-MM-DD) for the native input. The `value` and `onChange` props work with JavaScript `Date` objects for easier integration with your application logic.
