[ @sdxc/ui ]
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 timeZone = systemTimeZone();
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: toDayKey(picked.start, timeZone), end: toDayKey(picked.end, timeZone) } : 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(toDayKey(model.focusedDate, timeZone));
}),
// 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, { weekStartsOn: 0, timeZone }).map((week) => (
<RangeCalendar.Row key={week[0]?.key} mix={[flex()]}>
{week.map((day) => (
<RangeCalendar.Cell
key={day.key}
data-date={day.key}
data-outside-month={!day.inMonth || undefined}
tabIndex={day.key === toDayKey(model.focusedDate, timeZone) ? 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 remiximport "@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
Out of office
December 2026
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 29 | 30 | 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 | 6 | 7 | 8 | 9 |
Away from 2026-12-21 to 2026-12-31
View code
let timeZone = systemTimeZone();
let model = new CalendarModel({ focusedDate: OPENING_MONTH });
let range = { start: "2026-12-21", end: "2026-12-31" };
model.addEventListener("change", () => void handle.update());
function pickDay(key: string) {
let date = parseDayKey(key);
if (model.anchorDate === null) {
model.beginRange(date);
range = null;
} else {
let picked = model.completeRange(date);
if (picked) range = { start: toDayKey(picked.start, timeZone), end: toDayKey(picked.end, timeZone) };
}
void handle.update();
}
<div mix={[vstack({ gap: 3, align: "center" })]}>
<RangeCalendar aria-label="Out of office">
<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),
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(toDayKey(model.focusedDate, timeZone));
}),
]}
>
<RangeCalendar.GridHeader>
<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, { weekStartsOn: 0, timeZone }).map((week) => (
<RangeCalendar.Row key={week[0]?.key} mix={[flex()]}>
{week.map((day) => (
<RangeCalendar.Cell
key={day.key}
data-date={day.key}
data-outside-month={day.inMonth ? undefined : ""}
tabIndex={day.key === focusedKey ? 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>
<p mix={[m(0), text("xs"), fg("neutral.muted")]}>
{range ? `Away from ${range.start} to ${range.end}` : "Pick the day you are back"}
</p>
</div>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.