Skip to main content

RangePicker

Two-date selection — a start and an end — with optional presets.

import { RangePicker } from '@kalyx/react';

Basic usage

import { useState } from 'react';
import { RangePicker, type DateRange } from '@kalyx/react';

function Example() {
const [range, setRange] = useState<DateRange>({ start: null, end: null });
return (
<RangePicker value={range} onChange={setRange}>
<RangePicker.Input part="start" placeholder="Start" />
<RangePicker.Input part="end" placeholder="End" />
<RangePicker.Popover>
<RangePicker.Calendar />
</RangePicker.Popover>
</RangePicker>
);
}

Selection flow: first click sets start; second click sets end. If the second click is earlier than the first, the two swap automatically.

Try it live

Live Editor
function BasicRange() {
  const [range, setRange] = React.useState({ start: null, end: null });
  return (
    <RangePicker value={range} onChange={setRange}>
      <div className="kx-live-row">
        <RangePicker.Input part="start" className="kx-live-input" placeholder="Start" />
        <span aria-hidden></span>
        <RangePicker.Input part="end" className="kx-live-input" placeholder="End" />
      </div>
      <RangePicker.Popover className="kx-live-popover">
        <RangePicker.Calendar
          classNames={{
            header: 'kx-live-header',
            title: 'kx-live-title',
            navButton: 'kx-live-nav',
            grid: 'kx-live-grid',
            gridCell: 'kx-live-cell',
            weekdayHeader: 'kx-live-weekday',
            day: 'kx-live-day-range',
            dayRangeStart: 'kx-live-range-start',
            dayRangeEnd: 'kx-live-range-end',
            dayInRange: 'kx-live-inrange',
            dayToday: 'live-day-today',
            dayOutsideMonth: 'kx-live-outside',
            dayDisabled: 'kx-live-disabled',
          }}
        />
      </RangePicker.Popover>
      <div className="kx-live-value">
        <code>{range.start?.slice(0, 10) ?? 'null'}</code>
        <code>{range.end?.slice(0, 10) ?? 'null'}</code>
      </div>
    </RangePicker>
  );
}
Result
Loading...

<RangePicker> (Root)

PropTypeDefaultDescription
valueDateRangeControlled range.
defaultValueDateRangeUncontrolled initial range.
onChange(range: DateRange) => voidFires on every change (including partial).
disabledDisabledRule[] | booleanfalseDisable rules or entire picker.
readOnlybooleanfalsePrevents changes.
weekStartsOn0 | 10Week start.
displayFormatstring'yyyy-MM-dd'Format string.
localestring'en-US'BCP 47 locale.
displayTimezonestringIANA zone. When set, start/end are civil midnight of the clicked day in this zone. See Timezone.
adapterDateAdapterDateFnsAdapterCustom adapter.
labelsPartial<RangePickerLabels>Override ARIA labels. Adds startInput, endInput, presetsGroup on top of DatePicker's label keys.
childrenReactNodeSub-components.

DateRange

type DateRange = {
start: ISODateString | null;
end: ISODateString | null;
};

Kalyx doesn't enforce that end >= start while the user is selecting — it auto-swaps on commit. You get both values via onChange; the shape is always { start, end }.

<RangePicker.Input>

Two inputs required — one for start, one for end. The first input rendered acts as the Floating UI reference for the popover.

PropTypeDescription
part'start' | 'end'Required. Which side of the range this input manages.
formatstringOverrides Root's displayFormat.

<RangePicker.Popover>

Same behavior as DatePicker.Popover. role="dialog", outside-click dismissal, Escape support.

<RangePicker.Calendar>

Month grid with range highlighting.

PropTypeDescription
classNamesRangePickerCalendarClassNamesStyling.
type RangePickerCalendarClassNames = {
root?: string;
header?: string;
title?: string;
navButton?: string;
grid?: string;
gridRow?: string;
gridCell?: string;
day?: string;
daySelected?: string; // start or end day
dayInRange?: string; // days between start and end
dayToday?: string;
dayDisabled?: string;
dayOutsideMonth?: string;
weekdayHeader?: string;
};

<RangePicker.Presets>

Container for quick-pick buttons.

PropTypeDescription
classNamesRangePickerPresetsClassNamesStyling.
childrenReactNode<RangePicker.Preset> children.
type RangePickerPresetsClassNames = {
root?: string;
preset?: string;
presetActive?: string;
};

<RangePicker.Preset>

A single preset button. Provide either a built-in value key or a custom range:

PropTypeDescription
valuePresetKeyBuilt-in preset key.
rangeDateRangeCustom range (use instead of value).
childrenReactNodeButton label.

Built-in PresetKey

type PresetKey =
| 'today'
| 'yesterday'
| 'last7days'
| 'last30days'
| 'thisWeek'
| 'lastWeek'
| 'thisMonth'
| 'lastMonth'
| 'thisYear';

Patterns

<RangePicker value={range} onChange={setRange}>
<div className="flex gap-2">
<RangePicker.Input part="start" />
<RangePicker.Input part="end" />
</div>
<RangePicker.Popover className="flex gap-4 p-3">
<RangePicker.Presets className="flex flex-col gap-1">
<RangePicker.Preset value="today">Today</RangePicker.Preset>
<RangePicker.Preset value="last7days">Last 7 days</RangePicker.Preset>
<RangePicker.Preset value="last30days">Last 30 days</RangePicker.Preset>
<RangePicker.Preset value="thisMonth">This month</RangePicker.Preset>
</RangePicker.Presets>
<RangePicker.Calendar />
</RangePicker.Popover>
</RangePicker>
Live Editor
function RangeWithPresets() {
  const [range, setRange] = React.useState({ start: null, end: null });
  return (
    <RangePicker value={range} onChange={setRange}>
      <div className="kx-live-row">
        <RangePicker.Input part="start" className="kx-live-input" placeholder="Start" />
        <RangePicker.Input part="end" className="kx-live-input" placeholder="End" />
      </div>
      <RangePicker.Popover className="kx-live-popover kx-live-popover--split">
        <RangePicker.Presets
          classNames={{
            root: 'kx-live-presets',
            preset: 'kx-live-preset',
            presetActive: 'kx-live-preset-active',
          }}
        >
          <RangePicker.Preset value="today">Today</RangePicker.Preset>
          <RangePicker.Preset value="yesterday">Yesterday</RangePicker.Preset>
          <RangePicker.Preset value="last7days">Last 7 days</RangePicker.Preset>
          <RangePicker.Preset value="last30days">Last 30 days</RangePicker.Preset>
          <RangePicker.Preset value="thisMonth">This month</RangePicker.Preset>
          <RangePicker.Preset value="lastMonth">Last month</RangePicker.Preset>
        </RangePicker.Presets>
        <RangePicker.Calendar
          classNames={{
            header: 'kx-live-header',
            title: 'kx-live-title',
            navButton: 'kx-live-nav',
            grid: 'kx-live-grid',
            gridCell: 'kx-live-cell',
            weekdayHeader: 'kx-live-weekday',
            day: 'kx-live-day-range',
            dayRangeStart: 'kx-live-range-start',
            dayRangeEnd: 'kx-live-range-end',
            dayInRange: 'kx-live-inrange',
            dayToday: 'live-day-today',
            dayOutsideMonth: 'kx-live-outside',
            dayDisabled: 'kx-live-disabled',
          }}
        />
      </RangePicker.Popover>
    </RangePicker>
  );
}
Result
Loading...

Custom preset

<RangePicker.Preset
range={{
start: '2026-01-01T00:00:00.000Z',
end: '2026-06-30T00:00:00.000Z',
}}>
H1 2026
</RangePicker.Preset>

Constraining the range

<RangePicker
value={range}
onChange={setRange}
disabled={[{ before: '2026-01-01T00:00:00.000Z' }]}>
...
</RangePicker>

Event callbacks

PropSignatureFires when
onChange(range: DateRange) => voidThe range changes (start click, end click, preset, input).
onOpenChange(isOpen: boolean) => voidThe popover opens or closes. Not fired on initial mount.
onCalendarNavigate(viewMonth: ISODateString) => voidThe calendar view moves to a different month. Not fired on initial mount.