Calendar
A month-grid calendar surface composed from a navigable header and a native <table> of day cells, styled through the platform's own semantics.
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 |
Check in 2026-09-15 · weekends are unavailable, earlier dates are past
View code
let model = new CalendarModel({ focusedDate: OPENING_DAY });
let selected = dayKey(OPENING_DAY);
model.addEventListener("change", () => void handle.update());
let weeks = monthGrid(model.visibleMonth);
let days = weeks.flatMap((week) => week.days);
let monthLabel = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(
model.visibleMonth,
);
let focusedKey = dayKey(model.focusedDate);
let tabbable = days.find((day) => day.key === focusedKey) ?? days.find((day) => !day.outsideMonth);
<div mix={[vstack({ gap: 2, align: "start" })]}>
<Calendar aria-label="Pick a check-in date">
<Calendar.Header>
<Calendar.PreviousButton
aria-label="Previous month"
mix={[on<HTMLButtonElement, "click">("click", () => model.showPreviousMonth())]}
/>
<Calendar.Heading>{monthLabel}</Calendar.Heading>
<Calendar.NextButton
aria-label="Next month"
mix={[on<HTMLButtonElement, "click">("click", () => model.showNextMonth())]}
/>
</Calendar.Header>
<Calendar.Grid
aria-label={monthLabel}
mix={[
calendarKeys(model),
on<HTMLTableElement, "click">("click", (event) => {
let cell = (event.target as HTMLElement).closest("[data-date]");
if (!(cell instanceof HTMLElement) || cell.ariaDisabled === "true") return;
let day = days.find((candidate) => candidate.key === cell.dataset.date);
if (day === undefined) return;
selected = day.key;
void handle.update();
}),
]}
>
<Calendar.GridHeader>
<Calendar.Row>
{WEEKDAYS.map((weekday) => (
<Calendar.HeaderCell key={weekday}>{weekday}</Calendar.HeaderCell>
))}
</Calendar.Row>
</Calendar.GridHeader>
<Calendar.GridBody>
{weeks.map((week) => (
<Calendar.Row key={week.key}>
{week.days.map((day) => (
<Calendar.Cell
key={day.key}
data-date={day.key}
tabIndex={day.key === tabbable?.key ? 0 : -1}
aria-selected={day.key === selected ? "true" : undefined}
aria-disabled={day.date < OPENING_DAY ? "true" : undefined}
data-unavailable={
day.date.getDay() === 0 || day.date.getDay() === 6 ? "" : undefined
}
data-outside-month={day.outsideMonth ? "" : undefined}
>
{day.date.getDate()}
</Calendar.Cell>
))}
</Calendar.Row>
))}
</Calendar.GridBody>
</Calendar.Grid>
</Calendar>
<p mix={[text("sm"), fg("neutral")]}>
Check in {selected} · weekends are unavailable, earlier dates are past
</p>
</div>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 { Calendar } from "@sdxc/ui";Renders a rounded, padded surface around whichever children a consumer
composes; children left unset renders a native <input type="date">
through Input, the one part the platform itself operates.
Composition
The parts the component publishes, each a static property of the host.
CalendarCalendar.HeaderRenders Calendar's nav row: a flex row spacing Calendar.PreviousButton, Calendar.Heading, and Calendar.NextButton apart, the heading centered between the controls.Calendar.PreviousButtonRenders a round icon control standing for "show the previous month", sized to match Calendar.NextButton.Calendar.NextButtonRenders a round icon control standing for "show the following month", with the rendering, focus, and disabled-state contract of Calendar.PreviousButton and a trailing chevron by default.Calendar.HeadingRenders the consumer-formatted month/year caption through Heading, centered between the nav controls at a small semibold size layered after Heading's own defaults, so this size and alignment win over them.Calendar.GridRenders the day grid's native<table>host, rows and cells set apart by a small gap on every side.Calendar.GridHeaderRenders the grid's<thead>host: a plain wrapper for the weekday Calendar.Row.Calendar.RowRenders a native<tr>host: one row of Calendar.HeaderCell inside Calendar.GridHeader, or one week of Calendar.Cell inside Calendar.GridBody.Calendar.HeaderCellRenders a weekday abbreviation inside a native<th scope="col">, sized small and muted beneath Calendar.Cell's own larger day numbers.Calendar.GridBodyRenders the grid's<tbody>host: a plain wrapper for one Calendar.Row per week.Calendar.CellRenders one calendar day inside a native<td>, a fixed round pill around its number.
Examples
<Calendar aria-label={t("calendar.label")}>
<Calendar.Header>
<Calendar.PreviousButton aria-label={t("calendar.previous")} />
<Calendar.Heading>{monthLabel}</Calendar.Heading>
<Calendar.NextButton aria-label={t("calendar.next")} />
</Calendar.Header>
<Calendar.Grid aria-label={monthLabel}>
<Calendar.GridHeader>
<Calendar.Row>
{weekdayLabels.map((day) => (
<Calendar.HeaderCell key={day}>{day}</Calendar.HeaderCell>
))}
</Calendar.Row>
</Calendar.GridHeader>
<Calendar.GridBody>
{weeks.map((week) => (
<Calendar.Row key={week[0].iso}>
{week.map((day) => (
<Calendar.Cell
key={day.iso}
aria-selected={day.iso === selected ? "true" : undefined}
data-outside-month={day.outsideMonth ? "" : undefined}
>
{day.label}
</Calendar.Cell>
))}
</Calendar.Row>
))}
</Calendar.GridBody>
</Calendar.Grid>
</Calendar>Props
Read from the component's own types, so every prop, every default and every allowed value is listed.
| Prop | Type | Description |
|---|---|---|
color? | "brand" | "neutral" | "success" | "warning" | "danger" | Semantic color role for the bare fallback control's focus ring. Defaults to DEFAULT_COLOR. |
name? | string | Native name submitted with an enclosing form, read only by the bare fallback control. |
value? | string | Current value, in YYYY-MM-DD form, for a bare fallback control a consumer tracks itself. |
defaultValue? | string | Initial value, in YYYY-MM-DD form, for a bare fallback control left to the platform's own uncontrolled state. |
min? | string | Earliest accepted date, in YYYY-MM-DD form, read only by the bare fallback control. |
max? | string | Latest accepted date, in YYYY-MM-DD form, read only by the bare fallback control. |
step? | number | Granularity, in days, the bare fallback control's value must fall on. |
required? | boolean | Marks the bare fallback control required for its enclosing form. |
disabled? | boolean | Marks the bare fallback control inert and excluded from the tab order. |
readOnly? | boolean | Marks the bare fallback control's value fixed, while keeping it focusable and included in form submission. |
autoComplete? | string | Native autofill hint for the bare fallback control, e.g. "bday". |
Also accepts everything in TagProps<"div">.
Calendar.Header
Also accepts everything in TagProps<"header">.
Calendar.PreviousButton
Also accepts everything in TagProps<"button">.
Calendar.NextButton
Also accepts everything in TagProps<"button">.
Calendar.Heading
Also accepts everything in Heading.Props.
Calendar.Grid
Also accepts everything in TagProps<"table">.
Calendar.GridHeader
Also accepts everything in TagProps<"thead">.
Calendar.Row
Also accepts everything in TagProps<"tr">.
Calendar.HeaderCell
Also accepts everything in TagProps<"th">.
Calendar.GridBody
Also accepts everything in TagProps<"tbody">.
Calendar.Cell
Also accepts everything in TagProps<"td">.
Types
| Name | Values | What it decides |
|---|---|---|
Color | "brand" | "neutral" | "success" | "warning" | "danger" | Semantic color role for the bare <input type="date"> fallback's keyboard focus ring, each mapped to its matching --ui-* variables. |