RangeCalendar
A calendar surface for choosing a start and end date as one connected range, sharing every header, grid.
September 2026
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 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 |
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
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/uiimport "@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.
RangeCalendarRangeCalendar.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.
| Prop | Type | Description |
|---|---|---|
color? | Color | Semantic color role for the bare fallback pair's focus rings. Defaults to DEFAULT_COLOR. |
startName? | string | Native name submitted with an enclosing form, read only by the bare fallback's start-date input. |
endName? | string | Native name submitted with an enclosing form, read only by the bare fallback's end-date input. |
startValue? | string | Current start-of-range value, in YYYY-MM-DD form, for a bare fallback pair a consumer tracks itself. |
endValue? | string | Current end-of-range value, in YYYY-MM-DD form, for a bare fallback pair a consumer tracks itself. |
startDefaultValue? | string | Initial start-of-range value, in YYYY-MM-DD form, for a bare fallback pair left to the platform's own uncontrolled state. |
endDefaultValue? | string | Initial end-of-range value, in YYYY-MM-DD form, for a bare fallback pair left to the platform's own uncontrolled state. |
min? | string | Earliest accepted date, in YYYY-MM-DD form, read by both of the bare fallback pair's inputs. |
max? | string | Latest accepted date, in YYYY-MM-DD form, read by both of the bare fallback pair's inputs. |
step? | number | Granularity, in days, both of the bare fallback pair's values must fall on. |
required? | boolean | Marks both of the bare fallback pair's inputs required for their enclosing form. |
disabled? | boolean | Marks both of the bare fallback pair's inputs inert and excluded from the tab order. |
readOnly? | boolean | Marks both of the bare fallback pair's inputs' values fixed, while keeping them focusable and included in form submission. |
autoComplete? | string | Native autofill hint for both of the bare fallback pair's inputs, e.g. "bday". |
startLabel? | string | Accessible label for the bare fallback pair's start-date input. Read only when children is unset. |
endLabel? | string | Accessible label for the bare fallback pair's end-date input. Read only when children is unset. |
parts? | PartsProps | Per-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.