# Date field (Caseroom component)

The native date input in the field style, or a from–to range that keeps the end after the start. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

0.15% of the 2,062 sites in The UI Index have a date picker (3 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/date-field.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/date-field.json`
- HTML: https://www.themes.dev/caseroom/r/html/date-field.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/date-field/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### date-field.astro

```astro
---
import { cn } from '@/lib/utils';
import { dateField as c } from './date-field.variants';

interface Props {
  id: string;
  label: string;
  value?: string;
  min?: string;
  max?: string;
  hint?: string;
  name?: string;
  required?: boolean;
  /** A from–to pair in one fieldset; label is then the legend, and value/min/max are for the "from" field. */
  range?: { toValue?: string; fromLabel?: string; toLabel?: string };
  class?: string;
}
const { id, label, value, min, max, hint, name, required, range, class: className } = Astro.props;
---

{
  range ? (
    <fieldset data-date-range aria-describedby={hint ? `${id}-hint` : undefined} class={cn(c.range, className)}>
      <legend class={c.legend}>{label}</legend>
      <div class={c.pair}>
        <div class={c.root}>
          <label for={`${id}-from`} class={c.label}>{range.fromLabel ?? 'From'}</label>
          <input type="date" id={`${id}-from`} name={name ? `${name}-from` : undefined} value={value} min={min} max={max} required={required} data-from class={c.input} />
        </div>
        <div class={c.root}>
          <label for={`${id}-to`} class={c.label}>{range.toLabel ?? 'To'}</label>
          <input type="date" id={`${id}-to`} name={name ? `${name}-to` : undefined} value={range.toValue} min={value ?? min} max={max} required={required} data-to class={c.input} />
        </div>
      </div>
      {hint && <p id={`${id}-hint`} class={c.hint}>{hint}</p>}
    </fieldset>
  ) : (
    <div class={cn(c.root, className)}>
      <label for={id} class={c.label}>{label}</label>
      <input type="date" id={id} name={name} value={value} min={min} max={max} required={required} aria-describedby={hint ? `${id}-hint` : undefined} class={c.input} />
      {hint && <p id={`${id}-hint`} class={c.hint}>{hint}</p>}
    </div>
  )
}

<script>
  import './date-field.client';
</script>
```

### date-field.client.ts

```tsx
// Date range for Astro and plain HTML: in a [data-date-range], the "to" field's min follows the "from" value, and a
// "to" date before it moves up to it.
declare global {
  interface Window {
    __caseroomDate?: boolean;
  }
}

if (typeof window !== 'undefined' && !window.__caseroomDate) {
  window.__caseroomDate = true;
  document.addEventListener('change', (e) => {
    const from = e.target as HTMLInputElement;
    if (!from.matches?.('[data-date-range] input[data-from]')) return;
    const to = from.closest('[data-date-range]')!.querySelector<HTMLInputElement>('input[data-to]');
    if (!to) return;
    to.min = from.value;
    if (to.value && from.value && to.value < from.value) to.value = from.value;
  });
}

export {};
```

### date-field.tsx

```tsx
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { dateField as c } from './date-field.variants';

export interface DateFieldProps {
  id: string;
  label: string;
  defaultValue?: string;
  min?: string;
  max?: string;
  hint?: string;
  name?: string;
  required?: boolean;
  /** A from–to pair in one fieldset; label is then the legend, and defaultValue/min/max are for the "from" field. */
  range?: { toValue?: string; fromLabel?: string; toLabel?: string };
  className?: string;
}

/** A native date field in the field style, or a from–to pair whose "to" can't be before "from". */
export function DateField({ id, label, defaultValue, min, max, hint, name, required, range, className }: DateFieldProps) {
  const [from, setFrom] = useState(defaultValue);
  if (range)
    return (
      <fieldset data-date-range="" aria-describedby={hint ? `${id}-hint` : undefined} className={cn(c.range, className)}>
        <legend className={c.legend}>{label}</legend>
        <div className={c.pair}>
          <div className={c.root}>
            <label htmlFor={`${id}-from`} className={c.label}>{range.fromLabel ?? 'From'}</label>
            <input type="date" id={`${id}-from`} name={name ? `${name}-from` : undefined} defaultValue={defaultValue} min={min} max={max} required={required} data-from="" className={c.input} onChange={(e) => setFrom(e.target.value)} />
          </div>
          <div className={c.root}>
            <label htmlFor={`${id}-to`} className={c.label}>{range.toLabel ?? 'To'}</label>
            <input type="date" id={`${id}-to`} name={name ? `${name}-to` : undefined} defaultValue={range.toValue} min={from ?? min} max={max} required={required} data-to="" className={c.input} />
          </div>
        </div>
        {hint && <p id={`${id}-hint`} className={c.hint}>{hint}</p>}
      </fieldset>
    );
  return (
    <div className={cn(c.root, className)}>
      <label htmlFor={id} className={c.label}>{label}</label>
      <input type="date" id={id} name={name} defaultValue={defaultValue} min={min} max={max} required={required} aria-describedby={hint ? `${id}-hint` : undefined} className={c.input} />
      {hint && <p id={`${id}-hint`} className={c.hint}>{hint}</p>}
    </div>
  );
}
```

### date-field.variants.ts

```tsx
import { fieldClasses } from '../input/input.variants';

/** A date field on the native date input: the platform's own calendar, keyboard entry per part, the user's
 *  locale, min and max. Restyled to the field (the picker button becomes a quiet calendar mark). A range is two
 *  fields in a fieldset (from, to), with the "to" field's min kept at the "from" date. */
export const dateField = {
  root: 'grid gap-2',
  label: 'text-sm font-medium',
  // The ring is always there, transparent, and takes the ring colour on focus: focus sits in the day, month or year
  // part, and the browser applies a focus style there a frame late (forced-colours modes show the outline too).
  input: `${fieldClasses} h-9 w-full max-w-56 py-1 tabular-nums outline-2 outline-offset-0 outline-transparent [outline-style:solid] focus:border-ring focus:outline-ring [color-scheme:light] dark:[color-scheme:dark] [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:rounded-sm [&::-webkit-calendar-picker-indicator]:opacity-60 hover:[&::-webkit-calendar-picker-indicator]:opacity-100`,
  hint: 'text-sm text-muted-foreground',
  range: 'm-0 grid min-w-0 gap-2 border-0 p-0',
  legend: 'mb-2 p-0 text-sm font-medium',
  pair: 'flex flex-wrap items-end gap-3',
};
```
