implement
Range Calendar

Range Calendar

A month grid for picking a start and an end date.

Trip dates September 2026
SMTWTFS
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
import { signal } from "@implementjs/core";
import { today, type DateRange } from "@implementjs/primitives";
import { RangeCalendar } from "@/lib/components/ui/range-calendar";

export default function RangeCalendarDemo() {
	const value = signal<DateRange>({
		start: today(),
		end: today().add({ days: 4 }),
	});

	return RangeCalendar({ value, calendarLabel: "Trip dates" });
}

Installation

npx jsrepo add @implementjs/ui/range-calendar

jsrepo pulls button along with it, and installs @implementjs/lucide.

Usage

Assembled like the calendar — navigation, weekday header, and grid built in — with day styling for the shape of a range: rounded ends, a squared-off band through the middle, and a live highlight over the span being picked before the second click lands.

value is a DateRange: { start, end }, either of which can be null while a selection is in progress.

import { signal } from "@implementjs/core";
import { today, type DateRange } from "@implementjs/primitives";
import { RangeCalendar } from "@/lib/components/ui/range-calendar";

const value = signal<DateRange>({ start: today(), end: today().add({ days: 4 }) });

RangeCalendar({ value, calendarLabel: "Trip dates" });

Two months at a time

numberOfMonths: 2 is the usual shape for a date range picker; the styled root already lays the months out in a row on wider screens and stacks them below md.

RangeCalendar({ value, numberOfMonths: 2, calendarLabel: "Trip dates" });

What it shares with the calendar

The root wears the same border and padding as the calendar, written out in this file rather than borrowed from it, so the range calendar installs on its own. The day classes differ either way: a range needs states a single date has no use for.

The root is assembled, so the header, grid, and cell parts are not re-exported here. Import them from @implementjs/primitives when you want a different structure; Range Calendar covers them.

API Reference

Every prop the styling does not consume is forwarded to the Range Calendar primitive, so the tables below are the whole surface — the behavior props and the styling ones together.

RangeCalendar

A range calendar, fully assembled: month navigation, the weekday header, and the day grid are built in, with day styling for the start, middle, and end of a range. Every root prop below is forwarded. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<DateRange> | DateRange{ start: null, end: null }The selected range. Inverted writes are reordered. Pass a signal to control it from outside.
onValueChange(value: DateRange) => void—Runs whenever the range changes, including while only one end is picked.
minDaysnumber—The fewest days a range may span. Shorter picks restart the selection.
maxDaysnumber—The most days a range may span. Longer picks restart the selection.
excludeDisabledbooleanfalseClear the range if it would contain a disabled date.
onRangeSelect() => void—Runs after both ends of the range are selected.
placeholderSignal<CalendarDate> | CalendarDatetodayThe date the view starts on and keyboard focus follows. Pass a signal to control the view from outside.
minValueCalendarDate—The earliest selectable date. Earlier dates are disabled.
maxValueCalendarDate—The latest selectable date. Later dates are disabled.
isDateDisabled(date: CalendarDate) => boolean—Marks dates as disabled: not selectable and skipped by the keyboard.
isDateUnavailable(date: CalendarDate) => boolean—Marks dates as unavailable: focusable and rendered, but not selectable. Sets data-unavailable.
preventDeselectbooleanfalseWhether clicking a selected date again keeps it selected.
disabledReadable<boolean> | booleanfalseDisables the whole calendar.
readonlyReadable<boolean> | booleanfalseThe value can be read but not changed.
fixedWeeksbooleanfalseAlways render six weeks per month so the grid height never changes.
numberOfMonthsnumber1How many consecutive months are rendered.
pagedNavigationbooleanfalseWhether prev/next move by numberOfMonths months instead of one.
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6—The day the week starts on, 0 being Sunday. Defaults to the locale's week start where the runtime knows it.
weekdayFormat"narrow" | "short" | "long""narrow"The Intl width of the weekday names handed to the render function.
disableDaysOutsideMonthbooleantrueWhether the leading and trailing days outside the month are disabled.
localestring"en-US"BCP 47 locale tag used for all formatting.
calendarLabelstring"Event"Prefixed onto the visible month to label the calendar for assistive technology.
monthFormatIntl.DateTimeFormatOptions["month"] | ((month: number) => string)"long"How the heading and month select format month names.
yearFormatIntl.DateTimeFormatOptions["year"] | ((year: number) => string)"numeric"How the heading and year select format years.
Data attributeValue
[data-range-calendar-root]Present
[data-invalid]Present when an end is disabled/unavailable or the range is inverted
[data-disabled]Present when disabled
[data-readonly]Present when readonly