Accessible date range picker with one or two-month grid, hover preview, presets, and min/max constraints.
Controlled — value + onChange (React), v-model (Vue), [(value)] or forms (Angular) — or uncontrolled with defaultValue
One or two-month calendar with hover preview while picking
Auto-swap of from/to when the second pick precedes the first
Optional quick-select presets and clearable trigger
Surface-aware styling with FieldShell label/hint/error wiring
Status
stable
Category
forms
Since
v1.9.0
API
import { PixelDateRangePicker } from '@pxlkit/ui-kit';
Prop
Type
Default
Description
value
DateRangeValue
—
The range; leave unset for an uncontrolled picker.
defaultValue
DateRangeValue
—
Initial range while uncontrolled.
onChange
(next: DateRangeValue) => void
—
Called with the range after every change: its start alone after a first pick, {} once cleared.
min
Date
—
First day that can be picked.
max
Date
—
Last day that can be picked.
presets
{ label: string; value: { from: Date; to: Date } }[]
—
Quick picks of whole ranges, shown above the months.
numberOfMonths
1 | 2
2
Months shown side by side.
surface
'pixel' | 'linear'
—
Surface override; defaults to the nearest provider.
size
'sm' | 'md' | 'lg'
'md'
Trigger height.
label
string
—
Label rendered above the trigger.
hint
string
—
Helper text below the field; hidden while error is set.
error
string
—
Error message below the field; marks the trigger invalid.
placeholder
string
'Select date range'
Text shown while no range is picked.
clearable
boolean
false
Adds a clear button over the trigger's end and a Clear button under the months while a range is set.
name
string
—
Form field name — hidden inputs submit name.from and name.to as YYYY-MM-DD.
id
string
—
id of the trigger; generated when left out.
data-testid
string
—
data-testid of the trigger.
ref points to <button>.
Accessibility
WCAG target: 2.1 AA
two-month grid (role=grid)
aria-label on day cells
start/end announced
presets keyboard-reachable
clear button beside the trigger
Opening moves focus to the range start, else today; Escape, a preset and picking the end return it to the trigger. The popover is a dialog named "Choose date range". Each calendar panel uses role=grid with a labelled aria-live month header; day cells expose aria-selected for range edges, aria-current="date" for today and aria-disabled for out-of-bound days, and one day of the two months is in the tab order. Presets render as native buttons reachable via Tab. With clearable and a range set, a native "Clear range" button lies over the end of the trigger, beside it rather than inside (a button cannot contain a button): it is the next tab stop, clears the range, closes an open popover and moves focus to the trigger.
Keyboard
Key
Action
When
ArrowLeft
Move focus to the previous day
—
ArrowRight
Move focus to the next day
—
ArrowUp
Move focus to the previous week
—
ArrowDown
Move focus to the next week
—
Home
Move focus to the first day of the week
—
End
Move focus to the last day of the week
—
PageUp
Move focus to the same day of the previous month (its last day when shorter)
—
PageDown
Move focus to the same day of the next month (its last day when shorter)
—
Shift+PageUp
Move focus to the same day of the previous year
—
Shift+PageDown
Move focus to the same day of the next year
—
Enter
Select the focused day (start, then end)
—
Space
Select the focused day (start, then end)
—
Tab
Move from the trigger to its clear button while a range is set
clearable
Enter / Space
Clear the range and focus the trigger, without opening the popover
clear button focused
Usage
import { useState } from 'react';
import { PixelDateRangePicker, type DateRangeValue } from '@pxlkit/ui-kit';
export function Default() {
const [range, setRange] = useState<DateRangeValue>({});
return (
<PixelDateRangePicker
label="Date range"
value={range}
onChange={setRange}
placeholder="Select date range"
/>
);
}
Examples
Default
The calendar renders when the popover opens, in the browser: a server render holds the trigger alone, so no current month or today's mark can differ from the browser's.
import { useState } from 'react';
import { PixelDateRangePicker, type DateRangeValue } from '@pxlkit/ui-kit';
export function Default() {
const [range, setRange] = useState<DateRangeValue>({});
return (
<PixelDateRangePicker
label="Date range"
value={range}
onChange={setRange}
placeholder="Select date range"
/>
);
}
With Presets
import { useState } from 'react';
import { PixelDateRangePicker, type DateRangeValue } from '@pxlkit/ui-kit';
export function WithPresets() {
const [range, setRange] = useState<DateRangeValue>({});
const today = new Date();
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const last7 = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
const last30 = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
const next7 = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
return (
<PixelDateRangePicker
label="Reporting period"
value={range}
onChange={setRange}
clearable
presets={[
{ label: 'Last 7 days', value: { from: last7, to: start } },
{ label: 'Last 30 days', value: { from: last30, to: start } },
{ label: 'Next 7 days', value: { from: start, to: next7 } },
]}
/>
);
}
Single Month
import { useState } from 'react';
import { PixelDateRangePicker, type DateRangeValue } from '@pxlkit/ui-kit';
export function SingleMonth() {
const [range, setRange] = useState<DateRangeValue>({});
return (
<PixelDateRangePicker
label="Single-month view"
hint="Compact one-month calendar"
value={range}
onChange={setRange}
numberOfMonths={1}
clearable
/>
);
}