Skip to main content

React Hook Form

Kalyx stores values as ISO strings, which map cleanly to any form library. Here's the react-hook-form version.

Live: the shape you submit

react-hook-form isn't available in this live editor, so the example below uses useState to show the exact flow — onChange hands you an ISODateString | null, which is what RHF would store in field.value.

Live Editor
function FormFlow() {
  const [state, setState] = React.useState({ date: null, submitted: null });
  return (
    <form
      className="kx-live-form"
      onSubmit={(e) => {
        e.preventDefault();
        setState((s) => ({ ...s, submitted: s.date }));
      }}
    >
      <label htmlFor="startDate">Start date</label>
      <DatePicker
        value={state.date}
        onChange={(iso) => setState((s) => ({ ...s, date: iso }))}
      >
        <div className="kx-live-row">
          <DatePicker.Input id="startDate" className="kx-live-input" placeholder="Required" />
          <DatePicker.Trigger className="kx-live-trigger" aria-label="Open calendar" />
        </div>
        <DatePicker.Popover className="kx-live-popover">
          <DatePicker.Calendar
            classNames={{
              header: 'kx-live-header',
              title: 'kx-live-title',
              navButton: 'kx-live-nav',
              grid: 'kx-live-grid',
              weekdayHeader: 'kx-live-weekday',
              day: 'live-day',
              daySelected: 'live-day-selected',
              dayToday: 'live-day-today',
              dayOutsideMonth: 'kx-live-outside',
            }}
          />
        </DatePicker.Popover>
      </DatePicker>
      {!state.date && (
        <span className="kx-live-form-error" role="alert">
          Please pick a start date
        </span>
      )}
      <button type="submit" className="kx-live-submit" disabled={!state.date}>
        Submit
      </button>
      <div className="kx-live-value">
        field.value: <code>{state.date ?? 'null'}</code>
        <br />
        submitted: <code>{state.submitted ?? '—'}</code>
      </div>
    </form>
  );
}
Result
Loading...

Controlled with <Controller>

import { useForm, Controller } from 'react-hook-form';
import { DatePicker, type ISODateString } from '@kalyx/react';

type BookingForm = {
startDate: ISODateString | null;
};

export function BookingForm() {
const { control, handleSubmit } = useForm<BookingForm>({
defaultValues: { startDate: null },
});

return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="startDate"
control={control}
rules={{ required: 'Please pick a start date' }}
render={({ field, fieldState }) => (
<div>
<label htmlFor="startDate">Start date</label>
<DatePicker value={field.value} onChange={field.onChange}>
<DatePicker.Input
id="startDate"
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
{fieldState.error && (
<span role="alert">{fieldState.error.message}</span>
)}
</div>
)}
/>
<button type="submit">Book</button>
</form>
);
}

Since onChange receives an ISODateString | null, no transformation is needed before submit.

RangePicker with validation

<Controller
name="dateRange"
control={control}
rules={{
validate: (value) =>
(value?.start && value?.end) || 'Select both dates',
}}
render={({ field, fieldState }) => (
<RangePicker value={field.value} onChange={field.onChange}>
<RangePicker.Input part="start" onBlur={field.onBlur} ref={field.ref} />
<RangePicker.Input part="end" />
<RangePicker.Popover>
<RangePicker.Calendar />
</RangePicker.Popover>
{fieldState.error && <span role="alert">{fieldState.error.message}</span>}
</RangePicker>
)}
/>

Uncontrolled (no Controller)

For native-form submits you can skip Controller entirely:

<form action="/api/book" method="post">
<DatePicker defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input name="startDate" />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
<button>Submit</button>
</form>

The <input>'s value is the formatted display string. If you want to POST the ISO string instead, mirror the state into a hidden field.

Zod schema

import { z } from 'zod';

const isoString = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, 'Invalid date');

const BookingSchema = z.object({
startDate: isoString,
endDate: isoString,
});

Plug in via @hookform/resolvers/zod.