Skip to main content

RangePicker

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

RangePicker demo: selecting a start and end date with hover preview
Styling shown is demo-only — Kalyx ships zero CSS.
import { RangePicker } from '@kalyx/react';

Anatomy

<RangePicker> {/* Root — holds the { start, end } range */}
<RangePicker.Input part="start" /> {/* start-date combobox input */}
<RangePicker.Input part="end" /> {/* end-date combobox input */}
<RangePicker.Popover> {/* Floating-UI portal, role="dialog" */}
<RangePicker.Presets> {/* role="group" of quick-range buttons */}
<RangePicker.Preset /> {/* one quick-range toggle button */}
</RangePicker.Presets>
<RangePicker.Calendar /> {/* range-aware month grid */}
</RangePicker.Popover>
</RangePicker>

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

The live editor runs with React and all Kalyx components in scope, so import lines are omitted. Copy them in when porting to your project — see the full imports in the non-live blocks above.

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.
dir'ltr' | 'rtl''ltr'Layout direction. In 'rtl' the calendar grid carries dir="rtl" and ArrowLeft/ArrowRight are mirrored (WAI-ARIA grid pattern). See Internationalization.
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.
fixedWeeksboolean (default false)Always render 6 week rows. Without it the grid is 4–6 rows, so the popover changes height from month to month.
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;
};

data-* attributes

Day buttons emit range-aware state attributes (active-only). See Styling.

AttributeActive when
data-range-startDay is the range start.
data-range-endDay is the range end.
data-in-rangeDay is strictly between start and end.
data-todayDay is today.
data-focusedDay holds keyboard focus.
data-outside-monthDay belongs to an adjacent month.

<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.

Native form submission

Give each endpoint input a name. The visible formatted fields remain unnamed, and hidden fields submit the corresponding UTC-ISO values.

<RangePicker defaultValue={range}>
<RangePicker.Input part="start" name="startDate" />
<RangePicker.Input part="end" name="endDate" />
</RangePicker>