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 value="today">Today</RangePicker.Preset> {/* one quick-range toggle */}
</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
valueDateRange-Controlled range.
defaultValueDateRange-Uncontrolled initial range.
onChange(range: DateRange) => void-Fires on every change (including partial).
disabledDisabledRule[] | booleanfalseDisable rules or entire picker.
readOnlybooleanfalsePrevents changes.
weekStartsOn0 | 1inferred from localeWeek start. Inferred from locale when omitted; an explicit prop wins.
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.
displayTimezonestring-IANA 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.
childrenReactNode-Sub-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.
selectionMode'range' | 'week' (default 'range')'week' selects a whole week per click. WeekPicker.Calendar is this component with selectionMode pinned to 'week'.
weekAnchor'calendar' | 'clicked' (default 'calendar')Only with selectionMode="week". 'calendar' snaps to the weekStartsOn-aligned boundary; 'clicked' takes the 7-day span anchored on the clicked day.
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.
showWeekNumberboolean (default false)Render an ISO 8601 week-number column (1-53) on the left of the grid. The column is a <th scope="row"> outside the WAI-ARIA grid data region, so keyboard navigation across date cells is unaffected. Style it with the weekNumberHeader / weekNumber classNames keys.
type RangePickerCalendarClassNames = {
root?: string;
header?: string;
title?: string;
navButton?: string;
grid?: string;
gridRow?: string;
gridCell?: string;
day?: string;
dayRangeStart?: string; // the range's start day
dayRangeEnd?: string; // the range's end day
dayInRange?: string; // days between start and end
dayToday?: string;
dayDisabled?: string;
dayOutsideMonth?: string;
weekdayHeader?: string;
weekNumberHeader?: string; // only rendered when showWeekNumber is set
weekNumber?: string; // only rendered when showWeekNumber is set
};

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>