DateRangePicker
Chooses a span made of a start and an end day. Two calendars sit side by side, and the span previews under the pointer before the second click lands.
import { DateRangePicker } from 'neba';
<DateRangePicker label="Stay" startPlaceholder="Check in" endPlaceholder="Check out" clearable />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface: filled, hairline, or none |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| value | DateRange | null | — | The chosen range, as { start, end } with either end possibly null |
| defaultValue | DateRange | null | — | The initial range, for an uncontrolled picker |
| onValueChange | (value: DateRange) => void | — | Always called with an object. A cleared range is { start: null, end: null } |
| defaultMonth | Date | — | Which month the calendar opens on when there is no value. Defaults to this one |
| minDate | Date | null | — | The earliest day that may be chosen, in both panels |
| maxDate | Date | null | — | The other end of the same span |
| shouldDisableDate | (date: Date) => boolean | — | Blocks individual cells inside the range: weekends, holidays, a room already booked. The cell stays in the grid, unavailable, and the callback is handed the value that cell would produce |
| weekStartsOnshared | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | Which day the week starts on, Sunday being 0. Defaults to whatever the locale says |
| monthCount | 1 | 2 | 2 | How many months are on screen at once. Two by default, because a range that crosses a month boundary is the ordinary case |
| startPlaceholder | ReactNode | — | Shown in the first half of the trigger while the start is unchosen |
| endPlaceholder | ReactNode | — | The same, for the end |
| presets | readonly DateRangePreset[] | — | Shortcuts listed beside the calendars: "Last 7 days", "This month". A function value is computed when it is pressed |
| open | boolean | — | Whether the popup is open. Use with onOpenChange for a controlled one |
| defaultOpen | boolean | false | Whether it starts open |
| onOpenChange | (open: boolean) => void | — | Called when the popup opens or closes |
| locale | string | — | BCP 47 tag deciding the month and weekday names, the order of the header's two buttons, and how the trigger writes the value. Defaults to the browser's |
| format | Intl.DateTimeFormatOptions | — | How the trigger writes the value. Passed straight to Intl |
| clearable | boolean | false | Offers the × that empties the control |
| closeOnSelect | boolean | true | Closes the popup once both ends are chosen |
| labels | Partial<PickerLabels> | — | The strings a screen reader hears. One object rather than twenty props, because they are a set: they default to the locale’s wording, and the date names are not among them, those come from Intl |
| name | string | — | Identifies the field when a form is submitted. Two hidden inputs of the same name, so the ends arrive as FormData.getAll(name) |
| fullWidth | boolean | false | Stretches to the width of the container |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Native <div> attributes pass through to the root. Only color, defaultValue and children are excluded, since the table above spells them differently.
The value is a single object rather than a tuple.
interface DateRange {
start: Date | null;
end: Date | null;
}onValueChange is always called with an object, so a cleared range is { start: null, end: null } and there is never a second kind of empty to test for.
The half state between the first and second click is reported as { start, end: null }, and closing the popup without a second click throws it away. A second click that lands before the first is not an error: it is the same range in the other order, so the ends are sorted.
The remaining props (minDate · maxDate · shouldDisableDate · variant · size) behave as they do on DatePicker.
Examples
monthCount
Defaults to 2. The two panels are one calendar in halves: the left has no forward stepper, the right has no back stepper, and either header moves the pair.
With two panels, the leading and trailing days of neighbouring months are not drawn, so the same date never appears in both panels at once.
presets
Puts common spans beside the popup as buttons. A preset's value may be a range object or a function returning one. Prefer the function: a range computed once at render time is wrong for anyone who left the tab open.
name
name renders two hidden inputs of the same name, so both ends submit together.
const form = new FormData(event.currentTarget);
const [start, end] = form.getAll('stay'); // '2026-07-03', '2026-07-09'Both are local YYYY-MM-DD, the shape a native <input type="date"> submits.
Accessibility
- Each panel is a
role="grid"with a tab stop of its own, soTabmoves between the two grids rather than through eighty-four cells. - Only the two ends carry
aria-selected; the days between them get the band and nothing else. - The popup footer says which end the next click will fill.