KKiotaPay UI
Browse documentation

Date Input

A date field with two ways in: type the date, or pick it off a calendar. Optional age bounds constrain both.

Installation

pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/date.json

Installs components/custom/date.tsx, form-field, and the shadcn calendar and popover primitives. The calendar brings react-day-picker with it.

Usage

'use client'

import { DateInput } from '@/components/custom/date'

export function BirthdayField({ field }) {
  return (
    <DateInput
      label="Date of birth"
      required
      value={field.value}
      onChange={field.onChange}
      minAge={18}
      maxAge={100}
    />
  )
}

The export is DateInput, not Date. A component named Date shadows the global constructor in every file that imports it, so new Date() fails at runtime with nothing flagged at compile time.

Props

Extends React.ComponentProps<'input'> minus type, value and onChange.

PropTypeDefaultDescription
labelstring—Required.
valuestring''ISO date, yyyy-mm-dd.
onChange(value: string) => void—Receives the ISO string, not the event.
minAgenumber—Youngest allowed age in years. Bounds the calendar's upper edge.
maxAgenumber—Oldest allowed age in years. Bounds the lower edge.
min / maxstring—ISO bounds. Take precedence over the age props.
placeholderstring'yyyy-mm-dd'Shown while empty.
disabledbooleanfalseBlocks typing and the calendar.
errorbooleanfalseSwitches the border and ring to the danger ramp.
errorMessageReactNode—Shown only while error is true.

Also exports toIsoDate(date) and parseIsoDate(value) for callers that need the same conversion elsewhere.

Behaviour notes

Why both affordances. A date two weeks out is a glance at a calendar; a date of birth is thirty years of paging. Typing covers the second case, and the calendar's month and year dropdowns make it survivable for anyone who would rather click.

Typing commits on blur and Enter, never per keystroke. A half-typed 199 is not a date, and clearing the field on the way to a real one makes the value flicker and trips validation that runs on change. A value that will not parse, or falls outside the bounds, reverts to the last good one rather than being silently accepted.

Impossible dates are rejected. new Date('2025-02-31') rolls forward to 3 March instead of failing, so the parsed date is formatted back and compared to the input — if they differ, the input named a day that does not exist.

The calendar opens where the answer is. On the selected date if there is one, otherwise at the nearest allowed edge. A date-of-birth field bounded at eighteen opens eighteen years ago, not today.

Bounds are recomputed each render, so a tab left open across midnight does not keep yesterday's limits. They apply in three places at once: the calendar greys out disallowed days, the year dropdown stops at the boundary, and typed input outside the range is refused.

onChange receives a string, so react-hook-form's field.onChange and a useState setter both pass straight through.

Empty means empty. A blank field renders blank rather than defaulting to a boundary date — showing a value the form state does not hold makes a required field look answered until submit disagrees.