sdxc

Type to search, or start from one of these:

RangeCalendar

A calendar surface for choosing a start and end date as one connected range, sharing every header, grid.

September 2026

SunMonTueWedThuFriSat
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

Checking in 2026-09-14, out 2026-09-20

View code
let model = new CalendarModel({ focusedDate: OPENING_MONTH });
let range = { start: "2026-09-14", end: "2026-09-20" };

function pickDay(key: string) {
	let date = parseDayKey(key);

	if (model.anchorDate === null) {
		model.beginRange(date);
		range = null;
	} else {
		let picked = model.completeRange(date);
		range = picked ? { start: dayKey(picked.start), end: dayKey(picked.end) } : range;
	}

	void handle.update();
}

<RangeCalendar aria-label="Stay dates">
	<RangeCalendar.Header>
		<RangeCalendar.PreviousButton
			aria-label="Previous month"
			mix={[on<HTMLButtonElement, "click">("click", () => model.showPreviousMonth())]}
		/>
		<RangeCalendar.Heading>{monthLabel}</RangeCalendar.Heading>
		<RangeCalendar.NextButton
			aria-label="Next month"
			mix={[on<HTMLButtonElement, "click">("click", () => model.showNextMonth())]}
		/>
	</RangeCalendar.Header>

	<RangeCalendar.Grid
		aria-label={monthLabel}
		mix={[
			calendarKeys(model),
			rangePreview(model),
			on<HTMLTableElement, "click">("click", (event) => {
				if (!(event.target instanceof Element)) return;

				let key = event.target.closest("[data-date]")?.getAttribute("data-date");
				if (key) pickDay(key);
			}),
			on<HTMLTableElement, "keydown">("keydown", (event) => {
				if (event.key !== "Enter" && event.key !== " ") return;
				event.preventDefault();
				pickDay(dayKey(model.focusedDate));
			}),
			// RangeCalendar.Cell styles the committed range; the band the pointer
			// drags out between the two picks is the consumer's to paint.
			when("& td[data-range-preview]", [bg("brand.tint"), fg("brand.emphasis")]),
			when("& td[data-range-anchor]", [bg("brand.solid"), fg("brand.onSolid")]),
		]}
	>
		<RangeCalendar.GridHeader>
			{/* A day cell lays its own content out with flex, so each row is the flex line
			    the seven of them sit on. */}
			<RangeCalendar.Row mix={[flex()]}>
				{WEEKDAYS.map((day) => (
					<RangeCalendar.HeaderCell key={day} mix={[is(9)]}>
						{day}
					</RangeCalendar.HeaderCell>
				))}
			</RangeCalendar.Row>
		</RangeCalendar.GridHeader>

		<RangeCalendar.GridBody>
			{monthGrid(model.visibleMonth).map((week) => (
				<RangeCalendar.Row key={week.key} mix={[flex()]}>
					{week.days.map((day) => (
						<RangeCalendar.Cell
							key={day.key}
							data-date={day.key}
							data-outside-month={day.outsideMonth || undefined}
							tabIndex={day.key === dayKey(model.focusedDate) ? 0 : -1}
							aria-selected={inRange(day) ? "true" : undefined}
							data-selection-start={day.key === range?.start ? "" : undefined}
							data-selection-end={day.key === range?.end ? "" : undefined}
						>
							{day.date.getDate()}
						</RangeCalendar.Cell>
					))}
				</RangeCalendar.Row>
			))}
		</RangeCalendar.GridBody>
	</RangeCalendar.Grid>
</RangeCalendar>

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

npm add @sdxc/ui
import "@sdxc/ui/theme.css";

Usage

import { RangeCalendar } from "@sdxc/ui";

Renders RangeCalendar's root through Calendar. Composing RangeCalendar.Header and RangeCalendar.Grid as children renders the visual month surface; omitting children renders a bare, script-free native <input type="date"> pair from this instance's own fields.

Composition

The parts the component publishes, each a static property of the host.

  • RangeCalendar
  • RangeCalendar.CellRenders one calendar day for RangeCalendar, building on Calendar.Cell.

Examples

<RangeCalendar aria-label={t("calendar.label")}>
	<RangeCalendar.Header>
		<RangeCalendar.PreviousButton aria-label={t("calendar.previous")} />
		<RangeCalendar.Heading>{monthLabel}</RangeCalendar.Heading>
		<RangeCalendar.NextButton aria-label={t("calendar.next")} />
	</RangeCalendar.Header>
	<RangeCalendar.Grid aria-label={monthLabel}>
		<RangeCalendar.GridHeader>
			<RangeCalendar.Row>
				{weekdayLabels.map((day) => (
					<RangeCalendar.HeaderCell key={day}>{day}</RangeCalendar.HeaderCell>
				))}
			</RangeCalendar.Row>
		</RangeCalendar.GridHeader>
		<RangeCalendar.GridBody>
			{weeks.map((week) => (
				<RangeCalendar.Row key={week[0].iso}>
					{week.map((day) => (
						<RangeCalendar.Cell
							key={day.iso}
							aria-selected={day.inRange ? "true" : undefined}
							data-selection-start={day.iso === range.start ? "" : undefined}
							data-selection-end={day.iso === range.end ? "" : undefined}
						>
							{day.label}
						</RangeCalendar.Cell>
					))}
				</RangeCalendar.Row>
			))}
		</RangeCalendar.GridBody>
	</RangeCalendar.Grid>
</RangeCalendar>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeDescription
color?ColorSemantic color role for the bare fallback pair's focus rings. Defaults to DEFAULT_COLOR.
startName?stringNative name submitted with an enclosing form, read only by the bare fallback's start-date input.
endName?stringNative name submitted with an enclosing form, read only by the bare fallback's end-date input.
startValue?stringCurrent start-of-range value, in YYYY-MM-DD form, for a bare fallback pair a consumer tracks itself.
endValue?stringCurrent end-of-range value, in YYYY-MM-DD form, for a bare fallback pair a consumer tracks itself.
startDefaultValue?stringInitial start-of-range value, in YYYY-MM-DD form, for a bare fallback pair left to the platform's own uncontrolled state.
endDefaultValue?stringInitial end-of-range value, in YYYY-MM-DD form, for a bare fallback pair left to the platform's own uncontrolled state.
min?stringEarliest accepted date, in YYYY-MM-DD form, read by both of the bare fallback pair's inputs.
max?stringLatest accepted date, in YYYY-MM-DD form, read by both of the bare fallback pair's inputs.
step?numberGranularity, in days, both of the bare fallback pair's values must fall on.
required?booleanMarks both of the bare fallback pair's inputs required for their enclosing form.
disabled?booleanMarks both of the bare fallback pair's inputs inert and excluded from the tab order.
readOnly?booleanMarks both of the bare fallback pair's inputs' values fixed, while keeping them focusable and included in form submission.
autoComplete?stringNative autofill hint for both of the bare fallback pair's inputs, e.g. "bday".
startLabel?stringAccessible label for the bare fallback pair's start-date input. Read only when children is unset.
endLabel?stringAccessible label for the bare fallback pair's end-date input. Read only when children is unset.
parts?PartsPropsPer-part styling for the bare fallback pair's internally composed inputs.

Also accepts everything in TagProps<"div">.

RangeCalendar.Cell

Also accepts everything in Calendar.CellProps.