implement
Range Calendar

Range Calendar

Pick a span of days from a month grid.

Trip dates August 2026
SMTWTFS
26
27
28
29
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
31
1
2
3
4
5
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" });
}

A range calendar is a calendar that selects a span of days instead of a single date. The parts and layout are the same — the root takes a render function receiving months and weekdays — with RangeCalendarCell and RangeCalendarDay carrying the extra range state:

import {
	RangeCalendar,
	RangeCalendarCell,
	RangeCalendarDay,
	RangeCalendarGrid,
	RangeCalendarGridBody,
	RangeCalendarGridHead,
	RangeCalendarGridRow,
	RangeCalendarHeadCell,
	RangeCalendarHeader,
	RangeCalendarHeading,
	RangeCalendarNextButton,
	RangeCalendarPrevButton,
} from "@implementjs/primitives";
import { ForEach, Fragment } from "@implementjs/core";

RangeCalendar({ calendarLabel: "Trip dates" }, ({ months, weekdays }) =>
	Fragment(
		RangeCalendarHeader(
			RangeCalendarPrevButton("←"),
			RangeCalendarHeading(),
			RangeCalendarNextButton("→"),
		),
		ForEach(
			months,
			(month) => month.value.toString(),
			(month) =>
				RangeCalendarGrid(
					RangeCalendarGridHead(
						RangeCalendarGridRow(
							ForEach(
								weekdays,
								(_, i) => i,
								(weekday) => RangeCalendarHeadCell(weekday),
							),
						),
					),
					RangeCalendarGridBody(
						ForEach(
							month.bind((m) => m.weeks),
							(week) => week[0].toString(),
							(week) =>
								RangeCalendarGridRow(
									ForEach(
										week,
										(date) => date.toString(),
										(date) => RangeCalendarCell({ date, month }, RangeCalendarDay()),
									),
								),
						),
					),
				),
		),
	),
);

Ranges spanning several months read best with numberOfMonths: 2 — the render function receives both months, and the nav buttons page past them.

Value

The value is a DateRange{ start, end } of CalendarDate | null. Pass an object to seed it, or a signal to control it from outside:

const value = signal<DateRange>({ start: null, end: null });

RangeCalendar({ value }, ({ months, weekdays }) => /* ... */);

The first click sets start; while the end is undecided, hovering (or arrowing) highlights the prospective span with data-highlighted. The second click sets end — clicking before the start swaps the two so the range always runs forward, and a write of an inverted range from outside is reordered the same way. With a complete range, the next click starts a new one; clicking the end date again clears the selection (unless preventDeselect is set).

Constraints

  • minDays / maxDays bound the span's length. A pick outside the bounds restarts the selection at the clicked date.
  • excludeDisabled clears any range that would contain a disabled date; while selecting, an invalid span simply doesn't highlight, and completing across one restarts at the clicked date.
  • minValue, maxValue, isDateDisabled, and isDateUnavailable work exactly as on the calendar.

onRangeSelect runs whenever both ends become set.

Keyboard, navigation, i18n

Identical to the calendar: arrow keys move the focused day and page across months, Enter and Space select, prev/next buttons and the month/year selects navigate, and locale drives all formatting. Moving focus with the keyboard also drives the range highlight.

Styling

Parts set data-range-calendar-* attributes (data-range-calendar-root, data-range-calendar-day, …). On top of the shared cell state (data-selected, data-today, data-focused, data-outside-month, data-disabled, data-unavailable, data-value), cells and days expose the range shape:

RangeCalendarDay({
	class:
		"size-8 data-selection-start:bg-primary data-selection-end:bg-primary data-range-middle:bg-accent data-highlighted:bg-accent",
});
  • data-selection-start / data-selection-end mark the ends.
  • data-range-start / data-range-end mark the visual edges even while the end is unset.
  • data-range-middle marks days strictly inside a complete range.
  • data-highlighted marks the prospective span under the pointer or keyboard focus.

API Reference

RangeCalendar

The root. Owns the selected range and the visible months, and calls its children render function with { months, weekdays }. Sets role="application" and a full aria-label. 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.
minDaysnumberThe fewest days a range may span. Shorter picks restart the selection.
maxDaysnumberThe most days a range may span. Longer picks restart the selection.
excludeDisabledbooleanfalseClear the range if it would contain a disabled date.
onRangeSelect() => voidRuns 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.
minValueCalendarDateThe earliest selectable date. Earlier dates are disabled.
maxValueCalendarDateThe latest selectable date. Later dates are disabled.
isDateDisabled(date: CalendarDate) => booleanMarks dates as disabled: not selectable and skipped by the keyboard.
isDateUnavailable(date: CalendarDate) => booleanMarks dates as unavailable: focusable and rendered, but not selectable. Sets data-unavailable.
preventDeselectbooleanfalseWhether clicking a selected date again keeps it selected.
disabledSignal<boolean> | booleanfalseDisables the whole calendar.
readonlySignal<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 | 6The 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

RangeCalendarCell

One grid cell. Sets role="gridcell" plus aria-selected/aria-disabled, and computes the day's range state for everything inside it. Renders a Td; extra props are forwarded onto it.

PropTypeDefaultDescription
date*CalendarDate | Readable<CalendarDate>The date this cell renders.
month*CalendarDate | Month | Readable<CalendarDate | Month>The month whose grid the cell sits in — pass the render function's month straight through.
Data attributeValue
[data-range-calendar-cell]Present
[data-value]The cell's date as YYYY-MM-DD
[data-selected]Present when selected
[data-focused]Present when the placeholder is this date
[data-today]Present on today's date
[data-outside-month]Present on leading/trailing days
[data-outside-visible-months]Present when the date falls outside every rendered month
[data-disabled]Present when disabled
[data-unavailable]Present when unavailable
[data-selection-start]Present on the range's start date
[data-selection-end]Present on the range's end date
[data-range-start]Present on the visual start, even while the end is unset
[data-range-end]Present on the visual end, even while the end is unset
[data-range-middle]Present strictly inside a complete range
[data-highlighted]Present on the prospective span while the end is being picked

RangeCalendarDay

The selectable day inside a cell. Sets role="button" with a full date label; renders the day number unless children are passed. Hovering or focusing it drives the range highlight. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-day]Present
[data-value]The cell's date as YYYY-MM-DD
[data-selected]Present when selected
[data-focused]Present when the placeholder is this date
[data-today]Present on today's date
[data-outside-month]Present on leading/trailing days
[data-outside-visible-months]Present when the date falls outside every rendered month
[data-disabled]Present when disabled
[data-unavailable]Present when unavailable
[data-selection-start]Present on the range's start date
[data-selection-end]Present on the range's end date
[data-range-start]Present on the visual start, even while the end is unset
[data-range-end]Present on the visual end, even while the end is unset
[data-range-middle]Present strictly inside a complete range
[data-highlighted]Present on the prospective span while the end is being picked

RangeCalendarHeader

Wraps the heading and the nav buttons. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-header]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarHeading

Shows the visible month(s). Renders the formatted heading unless children are passed. aria-hidden — assistive technology hears the root's label instead. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-heading]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarPrevButton

Pages the view backwards. Disables itself when the previous page falls entirely before minValue. Renders a Button; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-prev-button]Present
[data-disabled]Present when disabled

RangeCalendarNextButton

Pages the view forwards. Disables itself when the next page falls entirely after maxValue. Renders a Button; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-next-button]Present
[data-disabled]Present when disabled

RangeCalendarGrid

One month's grid. Sets role="grid" and the aria disabled/readonly state. Renders a Table; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-grid]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarGridHead

Holds the weekday header row. Renders a Thead; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-grid-head]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarGridRow

One row: the weekday names in the head, a week in the body. Renders a Tr; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-grid-row]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarHeadCell

One weekday name. Render the strings from the weekdays render prop into these. Renders a Th; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-head-cell]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarGridBody

Holds the week rows. Renders a Tbody; extra props are forwarded onto it.

Data attributeValue
[data-range-calendar-grid-body]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

RangeCalendarMonthSelect

A native select that jumps the view to a month. Renders localized options for every month unless narrowed with the months prop. Renders a Select; extra props are forwarded onto it.

PropTypeDefaultDescription
monthsnumber[][1 … 12]The month numbers to offer.
monthFormatIntl.DateTimeFormatOptions["month"] | ((month: number) => string)How option labels are formatted. Defaults to the root's monthFormat.
disabledSignal<boolean> | booleanfalsePrevents changing the month.
Data attributeValue
[data-range-calendar-month-select]Present
[data-disabled]Present when disabled

RangeCalendarYearSelect

A native select that jumps the view to a year. Offers roughly the last hundred years through the next ten, bounded by minValue/maxValue. Renders a Select; extra props are forwarded onto it.

PropTypeDefaultDescription
yearsnumber[]The years to offer. Defaults to a window around the current year.
yearFormatIntl.DateTimeFormatOptions["year"] | ((year: number) => string)How option labels are formatted. Defaults to the root's yearFormat.
disabledSignal<boolean> | booleanfalsePrevents changing the year.
Data attributeValue
[data-range-calendar-year-select]Present
[data-disabled]Present when disabled